Skip to content

Commit 7b604d8

Browse files
[3.10] [doc] Update logging cookbook with an example of custom handli… (GH-98299)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 146232f commit 7b604d8

File tree

1 file changed

+283
-2
lines changed

1 file changed

+283
-2
lines changed

Doc/howto/logging-cookbook.rst

Lines changed: 283 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,211 @@ choose a different directory name for the log - just ensure that the directory e
276276
and that you have the permissions to create and update files in it.
277277

278278

279+
.. _custom-level-handling:
280+
281+
Custom handling of levels
282+
-------------------------
283+
284+
Sometimes, you might want to do something slightly different from the standard
285+
handling of levels in handlers, where all levels above a threshold get
286+
processed by a handler. To do this, you need to use filters. Let's look at a
287+
scenario where you want to arrange things as follows:
288+
289+
* Send messages of severity ``INFO`` and ``WARNING`` to ``sys.stdout``
290+
* Send messages of severity ``ERROR`` and above to ``sys.stderr``
291+
* Send messages of severity ``DEBUG`` and above to file ``app.log``
292+
293+
Suppose you configure logging with the following JSON:
294+
295+
.. code-block:: json
296+
297+
{
298+
"version": 1,
299+
"disable_existing_loggers": false,
300+
"formatters": {
301+
"simple": {
302+
"format": "%(levelname)-8s - %(message)s"
303+
}
304+
},
305+
"handlers": {
306+
"stdout": {
307+
"class": "logging.StreamHandler",
308+
"level": "INFO",
309+
"formatter": "simple",
310+
"stream": "ext://sys.stdout",
311+
},
312+
"stderr": {
313+
"class": "logging.StreamHandler",
314+
"level": "ERROR",
315+
"formatter": "simple",
316+
"stream": "ext://sys.stderr"
317+
},
318+
"file": {
319+
"class": "logging.FileHandler",
320+
"formatter": "simple",
321+
"filename": "app.log",
322+
"mode": "w"
323+
}
324+
},
325+
"root": {
326+
"level": "DEBUG",
327+
"handlers": [
328+
"stderr",
329+
"stdout",
330+
"file"
331+
]
332+
}
333+
}
334+
335+
This configuration does *almost* what we want, except that ``sys.stdout`` would
336+
show messages of severity ``ERROR`` and above as well as ``INFO`` and
337+
``WARNING`` messages. To prevent this, we can set up a filter which excludes
338+
those messages and add it to the relevant handler. This can be configured by
339+
adding a ``filters`` section parallel to ``formatters`` and ``handlers``:
340+
341+
.. code-block:: json
342+
343+
"filters": {
344+
"warnings_and_below": {
345+
"()" : "__main__.filter_maker",
346+
"level": "WARNING"
347+
}
348+
}
349+
350+
and changing the section on the ``stdout`` handler to add it:
351+
352+
.. code-block:: json
353+
354+
"stdout": {
355+
"class": "logging.StreamHandler",
356+
"level": "INFO",
357+
"formatter": "simple",
358+
"stream": "ext://sys.stdout",
359+
"filters": ["warnings_and_below"]
360+
}
361+
362+
A filter is just a function, so we can define the ``filter_maker`` (a factory
363+
function) as follows:
364+
365+
.. code-block:: python
366+
367+
def filter_maker(level):
368+
level = getattr(logging, level)
369+
370+
def filter(record):
371+
return record.levelno <= level
372+
373+
return filter
374+
375+
This converts the string argument passed in to a numeric level, and returns a
376+
function which only returns ``True`` if the level of the passed in record is
377+
at or below the specified level. Note that in this example I have defined the
378+
``filter_maker`` in a test script ``main.py`` that I run from the command line,
379+
so its module will be ``__main__`` - hence the ``__main__.filter_maker`` in the
380+
filter configuration. You will need to change that if you define it in a
381+
different module.
382+
383+
With the filter added, we can run ``main.py``, which in full is:
384+
385+
.. code-block:: python
386+
387+
import json
388+
import logging
389+
import logging.config
390+
391+
CONFIG = '''
392+
{
393+
"version": 1,
394+
"disable_existing_loggers": false,
395+
"formatters": {
396+
"simple": {
397+
"format": "%(levelname)-8s - %(message)s"
398+
}
399+
},
400+
"filters": {
401+
"warnings_and_below": {
402+
"()" : "__main__.filter_maker",
403+
"level": "WARNING"
404+
}
405+
},
406+
"handlers": {
407+
"stdout": {
408+
"class": "logging.StreamHandler",
409+
"level": "INFO",
410+
"formatter": "simple",
411+
"stream": "ext://sys.stdout",
412+
"filters": ["warnings_and_below"]
413+
},
414+
"stderr": {
415+
"class": "logging.StreamHandler",
416+
"level": "ERROR",
417+
"formatter": "simple",
418+
"stream": "ext://sys.stderr"
419+
},
420+
"file": {
421+
"class": "logging.FileHandler",
422+
"formatter": "simple",
423+
"filename": "app.log",
424+
"mode": "w"
425+
}
426+
},
427+
"root": {
428+
"level": "DEBUG",
429+
"handlers": [
430+
"stderr",
431+
"stdout",
432+
"file"
433+
]
434+
}
435+
}
436+
'''
437+
438+
def filter_maker(level):
439+
level = getattr(logging, level)
440+
441+
def filter(record):
442+
return record.levelno <= level
443+
444+
return filter
445+
446+
logging.config.dictConfig(json.loads(CONFIG))
447+
logging.debug('A DEBUG message')
448+
logging.info('An INFO message')
449+
logging.warning('A WARNING message')
450+
logging.error('An ERROR message')
451+
logging.critical('A CRITICAL message')
452+
453+
And after running it like this:
454+
455+
.. code-block:: shell
456+
457+
python main.py 2>stderr.log >stdout.log
458+
459+
We can see the results are as expected:
460+
461+
.. code-block:: shell
462+
463+
$ more *.log
464+
::::::::::::::
465+
app.log
466+
::::::::::::::
467+
DEBUG - A DEBUG message
468+
INFO - An INFO message
469+
WARNING - A WARNING message
470+
ERROR - An ERROR message
471+
CRITICAL - A CRITICAL message
472+
::::::::::::::
473+
stderr.log
474+
::::::::::::::
475+
ERROR - An ERROR message
476+
CRITICAL - A CRITICAL message
477+
::::::::::::::
478+
stdout.log
479+
::::::::::::::
480+
INFO - An INFO message
481+
WARNING - A WARNING message
482+
483+
279484
Configuration server example
280485
----------------------------
281486

@@ -564,7 +769,7 @@ To run a logging listener in production, you may need to use a process-managemen
564769
such as `Supervisor <http://supervisord.org/>`_. `Here
565770
<https://gist.github.com/vsajip/4b227eeec43817465ca835ca66f75e2b>`_ is a Gist which
566771
provides the bare-bones files to run the above functionality using Supervisor: you
567-
will need to change the `/path/to/` parts in the Gist to reflect the actual paths you
772+
will need to change the ``/path/to/`` parts in the Gist to reflect the actual paths you
568773
want to use.
569774

570775

@@ -2776,7 +2981,7 @@ Formatting times using UTC (GMT) via configuration
27762981
--------------------------------------------------
27772982

27782983
Sometimes you want to format times using UTC, which can be done using a class
2779-
such as `UTCFormatter`, shown below::
2984+
such as ``UTCFormatter``, shown below::
27802985

27812986
import logging
27822987
import time
@@ -3430,6 +3635,82 @@ the above handler, you'd pass structured data using something like this::
34303635
i = 1
34313636
logger.debug('Message %d', i, extra=extra)
34323637

3638+
How to treat a logger like an output stream
3639+
-------------------------------------------
3640+
3641+
Sometimes, you need to interface to a third-party API which expects a file-like
3642+
object to write to, but you want to direct the API's output to a logger. You
3643+
can do this using a class which wraps a logger with a file-like API.
3644+
Here's a short script illustrating such a class:
3645+
3646+
.. code-block:: python
3647+
3648+
import logging
3649+
3650+
class LoggerWriter:
3651+
def __init__(self, logger, level):
3652+
self.logger = logger
3653+
self.level = level
3654+
3655+
def write(self, message):
3656+
if message != '\n': # avoid printing bare newlines, if you like
3657+
self.logger.log(self.level, message)
3658+
3659+
def flush(self):
3660+
# doesn't actually do anything, but might be expected of a file-like
3661+
# object - so optional depending on your situation
3662+
pass
3663+
3664+
def close(self):
3665+
# doesn't actually do anything, but might be expected of a file-like
3666+
# object - so optional depending on your situation. You might want
3667+
# to set a flag so that later calls to write raise an exception
3668+
pass
3669+
3670+
def main():
3671+
logging.basicConfig(level=logging.DEBUG)
3672+
logger = logging.getLogger('demo')
3673+
info_fp = LoggerWriter(logger, logging.INFO)
3674+
debug_fp = LoggerWriter(logger, logging.DEBUG)
3675+
print('An INFO message', file=info_fp)
3676+
print('A DEBUG message', file=debug_fp)
3677+
3678+
if __name__ == "__main__":
3679+
main()
3680+
3681+
When this script is run, it prints
3682+
3683+
.. code-block:: text
3684+
3685+
INFO:demo:An INFO message
3686+
DEBUG:demo:A DEBUG message
3687+
3688+
You could also use ``LoggerWriter`` to redirect ``sys.stdout`` and
3689+
``sys.stderr`` by doing something like this:
3690+
3691+
.. code-block:: python
3692+
3693+
import sys
3694+
3695+
sys.stdout = LoggerWriter(logger, logging.INFO)
3696+
sys.stderr = LoggerWriter(logger, logging.WARNING)
3697+
3698+
You should do this *after* configuring logging for your needs. In the above
3699+
example, the :func:`~logging.basicConfig` call does this (using the
3700+
``sys.stderr`` value *before* it is overwritten by a ``LoggerWriter``
3701+
instance). Then, you'd get this kind of result:
3702+
3703+
.. code-block:: pycon
3704+
3705+
>>> print('Foo')
3706+
INFO:demo:Foo
3707+
>>> print('Bar', file=sys.stderr)
3708+
WARNING:demo:Bar
3709+
>>>
3710+
3711+
Of course, these above examples show output according to the format used by
3712+
:func:`~logging.basicConfig`, but you can use a different formatter when you
3713+
configure logging.
34333714

34343715
.. patterns-to-avoid:
34353716

0 commit comments

Comments
 (0)