@@ -276,6 +276,211 @@ choose a different directory name for the log - just ensure that the directory e
276
276
and that you have the permissions to create and update files in it.
277
277
278
278
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
+
279
484
Configuration server example
280
485
----------------------------
281
486
@@ -3420,6 +3625,159 @@ the above handler, you'd pass structured data using something like this::
3420
3625
i = 1
3421
3626
logger.debug('Message %d', i, extra=extra)
3422
3627
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.
3423
3781
3424
3782
.. patterns-to-avoid:
3425
3783
@@ -3431,7 +3789,6 @@ need to do or deal with, it is worth mentioning some usage patterns which are
3431
3789
*unhelpful *, and which should therefore be avoided in most cases. The following
3432
3790
sections are in no particular order.
3433
3791
3434
-
3435
3792
Opening the same log file multiple times
3436
3793
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3437
3794
@@ -3480,7 +3837,6 @@ that in other languages such as Java and C#, loggers are often static class
3480
3837
attributes. However, this pattern doesn't make sense in Python, where the
3481
3838
module (and not the class) is the unit of software decomposition.
3482
3839
3483
-
3484
3840
Adding handlers other than :class: `NullHandler ` to a logger in a library
3485
3841
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3486
3842
@@ -3489,7 +3845,6 @@ responsibility of the application developer, not the library developer. If you
3489
3845
are maintaining a library, ensure that you don't add handlers to any of your
3490
3846
loggers other than a :class: `~logging.NullHandler ` instance.
3491
3847
3492
-
3493
3848
Creating a lot of loggers
3494
3849
^^^^^^^^^^^^^^^^^^^^^^^^^
3495
3850
0 commit comments