@@ -276,6 +276,211 @@ choose a different directory name for the log - just ensure that the directory e
276276and 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
@@ -3420,6 +3625,159 @@ the above handler, you'd pass structured data using something like this::
34203625 i = 1
34213626 logger.debug('Message %d', i, extra=extra)
34223627
3628+ How to treat a logger like an output stream
3629+ -------------------------------------------
3630+
3631+ Sometimes, you need to interface to a third-party API which expects a file-like
3632+ object to write to, but you want to direct the API's output to a logger. You
3633+ can do this using a class which wraps a logger with a file-like API.
3634+ Here's a short script illustrating such a class:
3635+
3636+ .. code-block :: python
3637+
3638+ import logging
3639+
3640+ class LoggerWriter :
3641+ def __init__ (self , logger , level ):
3642+ self .logger = logger
3643+ self .level = level
3644+
3645+ def write (self , message ):
3646+ if message != ' \n ' : # avoid printing bare newlines, if you like
3647+ self .logger.log(self .level, message)
3648+
3649+ def flush (self ):
3650+ # doesn't actually do anything, but might be expected of a file-like
3651+ # object - so optional depending on your situation
3652+ pass
3653+
3654+ def close (self ):
3655+ # doesn't actually do anything, but might be expected of a file-like
3656+ # object - so optional depending on your situation. You might want
3657+ # to set a flag so that later calls to write raise an exception
3658+ pass
3659+
3660+ def main ():
3661+ logging.basicConfig(level = logging.DEBUG )
3662+ logger = logging.getLogger(' demo' )
3663+ info_fp = LoggerWriter(logger, logging.INFO )
3664+ debug_fp = LoggerWriter(logger, logging.DEBUG )
3665+ print (' An INFO message' , file = info_fp)
3666+ print (' A DEBUG message' , file = debug_fp)
3667+
3668+ if __name__ == " __main__" :
3669+ main()
3670+
3671+ When this script is run, it prints
3672+
3673+ .. code-block :: text
3674+
3675+ INFO:demo:An INFO message
3676+ DEBUG:demo:A DEBUG message
3677+
3678+ You could also use ``LoggerWriter `` to redirect ``sys.stdout `` and
3679+ ``sys.stderr `` by doing something like this:
3680+
3681+ .. code-block :: python
3682+
3683+ import sys
3684+
3685+ sys.stdout = LoggerWriter(logger, logging.INFO )
3686+ sys.stderr = LoggerWriter(logger, logging.WARNING )
3687+
3688+ You should do this *after * configuring logging for your needs. In the above
3689+ example, the :func: `~logging.basicConfig ` call does this (using the
3690+ ``sys.stderr `` value *before * it is overwritten by a ``LoggerWriter ``
3691+ instance). Then, you'd get this kind of result:
3692+
3693+ .. code-block :: pycon
3694+
3695+ >>> print('Foo')
3696+ INFO:demo:Foo
3697+ >>> print('Bar', file=sys.stderr)
3698+ WARNING:demo:Bar
3699+ >>>
3700+
3701+ Of course, these above examples show output according to the format used by
3702+ :func: `~logging.basicConfig `, but you can use a different formatter when you
3703+ configure logging.
3704+ =======
3705+ How to treat a logger like an output stream
3706+ -------------------------------------------
3707+
3708+ Sometimes, you need to interface to a third-party API which expects a file-like
3709+ object to write to, but you want to direct the API's output to a logger. You
3710+ can do this using a class which wraps a logger with a file-like API.
3711+ Here's a short script illustrating such a class:
3712+
3713+ .. code-block :: python
3714+
3715+ import logging
3716+
3717+ class LoggerWriter :
3718+ def __init__ (self , logger , level ):
3719+ self .logger = logger
3720+ self .level = level
3721+
3722+ def write (self , message ):
3723+ if message != ' \n ' : # avoid printing bare newlines, if you like
3724+ self .logger.log(self .level, message)
3725+
3726+ def flush (self ):
3727+ # doesn't actually do anything, but might be expected of a file-like
3728+ # object - so optional depending on your situation
3729+ pass
3730+
3731+ def close (self ):
3732+ # doesn't actually do anything, but might be expected of a file-like
3733+ # object - so optional depending on your situation. You might want
3734+ # to set a flag so that later calls to write raise an exception
3735+ pass
3736+
3737+ def main ():
3738+ logging.basicConfig(level = logging.DEBUG )
3739+ logger = logging.getLogger(' demo' )
3740+ info_fp = LoggerWriter(logger, logging.INFO )
3741+ debug_fp = LoggerWriter(logger, logging.DEBUG )
3742+ print (' An INFO message' , file = info_fp)
3743+ print (' A DEBUG message' , file = debug_fp)
3744+
3745+ if __name__ == " __main__" :
3746+ main()
3747+
3748+ When this script is run, it prints
3749+
3750+ .. code-block :: text
3751+
3752+ INFO:demo:An INFO message
3753+ DEBUG:demo:A DEBUG message
3754+
3755+ You could also use ``LoggerWriter `` to redirect ``sys.stdout `` and
3756+ ``sys.stderr `` by doing something like this:
3757+
3758+ .. code-block :: python
3759+
3760+ import sys
3761+
3762+ sys.stdout = LoggerWriter(logger, logging.INFO )
3763+ sys.stderr = LoggerWriter(logger, logging.WARNING )
3764+
3765+ You should do this *after * configuring logging for your needs. In the above
3766+ example, the :func: `~logging.basicConfig ` call does this (using the
3767+ ``sys.stderr `` value *before * it is overwritten by a ``LoggerWriter ``
3768+ instance). Then, you'd get this kind of result:
3769+
3770+ .. code-block :: pycon
3771+
3772+ >>> print('Foo')
3773+ INFO:demo:Foo
3774+ >>> print('Bar', file=sys.stderr)
3775+ WARNING:demo:Bar
3776+ >>>
3777+
3778+ Of course, the examples above show output according to the format used by
3779+ :func: `~logging.basicConfig `, but you can use a different formatter when you
3780+ configure logging.
34233781
34243782.. patterns-to-avoid:
34253783
@@ -3431,7 +3789,6 @@ need to do or deal with, it is worth mentioning some usage patterns which are
34313789*unhelpful *, and which should therefore be avoided in most cases. The following
34323790sections are in no particular order.
34333791
3434-
34353792Opening the same log file multiple times
34363793^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34373794
@@ -3480,7 +3837,6 @@ that in other languages such as Java and C#, loggers are often static class
34803837attributes. However, this pattern doesn't make sense in Python, where the
34813838module (and not the class) is the unit of software decomposition.
34823839
3483-
34843840Adding handlers other than :class: `NullHandler ` to a logger in a library
34853841^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34863842
@@ -3489,7 +3845,6 @@ responsibility of the application developer, not the library developer. If you
34893845are maintaining a library, ensure that you don't add handlers to any of your
34903846loggers other than a :class: `~logging.NullHandler ` instance.
34913847
3492-
34933848Creating a lot of loggers
34943849^^^^^^^^^^^^^^^^^^^^^^^^^
34953850
0 commit comments