Skip to content

Commit 79c0ade

Browse files
[3.11] [doc] Update logging cookbook with an example of custom handli… (GH-98296)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent d3b57dc commit 79c0ade

File tree

1 file changed

+206
-4
lines changed

1 file changed

+206
-4
lines changed

Doc/howto/logging-cookbook.rst

+206-4
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

@@ -3503,7 +3708,7 @@ instance). Then, you'd get this kind of result:
35033708
WARNING:demo:Bar
35043709
>>>
35053710
3506-
Of course, these above examples show output according to the format used by
3711+
Of course, the examples above show output according to the format used by
35073712
:func:`~logging.basicConfig`, but you can use a different formatter when you
35083713
configure logging.
35093714

@@ -3517,7 +3722,6 @@ need to do or deal with, it is worth mentioning some usage patterns which are
35173722
*unhelpful*, and which should therefore be avoided in most cases. The following
35183723
sections are in no particular order.
35193724

3520-
35213725
Opening the same log file multiple times
35223726
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35233727

@@ -3566,7 +3770,6 @@ that in other languages such as Java and C#, loggers are often static class
35663770
attributes. However, this pattern doesn't make sense in Python, where the
35673771
module (and not the class) is the unit of software decomposition.
35683772

3569-
35703773
Adding handlers other than :class:`NullHandler` to a logger in a library
35713774
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35723775

@@ -3575,7 +3778,6 @@ responsibility of the application developer, not the library developer. If you
35753778
are maintaining a library, ensure that you don't add handlers to any of your
35763779
loggers other than a :class:`~logging.NullHandler` instance.
35773780

3578-
35793781
Creating a lot of loggers
35803782
^^^^^^^^^^^^^^^^^^^^^^^^^
35813783

0 commit comments

Comments
 (0)