@@ -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
@@ -564,7 +769,7 @@ To run a logging listener in production, you may need to use a process-managemen
564
769
such as `Supervisor <http://supervisord.org/ >`_. `Here
565
770
<https://gist.github.com/vsajip/4b227eeec43817465ca835ca66f75e2b> `_ is a Gist which
566
771
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
568
773
want to use.
569
774
570
775
@@ -2776,7 +2981,7 @@ Formatting times using UTC (GMT) via configuration
2776
2981
--------------------------------------------------
2777
2982
2778
2983
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::
2780
2985
2781
2986
import logging
2782
2987
import time
@@ -3430,6 +3635,82 @@ the above handler, you'd pass structured data using something like this::
3430
3635
i = 1
3431
3636
logger.debug('Message %d', i, extra=extra)
3432
3637
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.
3433
3714
3434
3715
.. patterns-to-avoid:
3435
3716
0 commit comments