Defining handlers for multiple destinations

We have several use cases for sending the log output to multiple destinations, which are shown in the following bullet list:

  • We might want duplicate logs to improve the reliability of operations.
  • We might be using sophisticated Filter objects to create distinct subsets of messages.
  • We might have different levels for each destination. We can use the debugging level to separate debugging messages from informational messages.
  • We might have different handlers based on the Logger names to represent different foci.

Of course, we can also combine these different choices to create quite complex scenarios. In order to create multiple destinations, we must create multiple Handler instances. Each Handler might contain a customized Formatter; it could contain an optional level, and an optional list of filters that can be applied.

Once we have multiple Handler instances, we can bind one or more Logger objects to the desired Handler instances. A Handler object can have a level filter. Using this, we can have multiple handler instances; each can have a different filter to show different groups of messages based on the level. Also, we can explicitly create Filter objects if we need even more sophisticated filtering than the built-in filters, which only check the severity level.

While we can configure this through the logging module API, it's often more clear to define most of the logging details in a separate configuration file. One elegant way to handle this is to use YAML notation for a configuration dictionary. We can then load the dictionary with a relatively straightforward use of logging.config.dictConfig(yaml.load(somefile)).

The YAML notation is somewhat more compact than the notation accepted by configparser. The documentation for logging.config in the Python standard library uses YAML examples because of their clarity. We'll follow this pattern.

Here's an example of a configuration file with two handlers and two families of loggers:

version: 1 
handlers: 
  console: 
    class: logging.StreamHandler 
    stream: ext://sys.stderr 
    formatter: basic 
  audit_file: 
    class: logging.FileHandler 
    filename: data/ch16_audit.log 
    encoding: utf-8 
    formatter: basic 
formatters: 
  basic: 
    style: "{" 
    format: "{levelname:s}:{name:s}:{message:s}" 
loggers: 
  verbose: 
    handlers: [console] 
    level: INFO 
  audit: 
    handlers: [audit_file] 
    level: INFO 

We defined two handlers: console and audit_file. The console is StreamHandler that is sent to sys.stderr. Note that we have to use a URI-style syntax of ext://sys.stderr to name an external Python resource. In this context, external means external to the configuration file. This complex string is mapped to the sys.stderr object. The audit_file is FileHandler that will write to a given file. By default, files are opened with a mode of a to append.

We also defined the formatter, named basic, with a format to produce messages that match the ones created when logging is configured via basicConfig(). If we don't use this, the default format used by dictConfig() only has the message text.

Finally, we defined two top-level loggers, verbose and audit. The verbose instance will be used by all the loggers that have a top-level name of verbose. We can then use a Logger name such as verbose.example.SomeClass to create an instance that is a child of verbose. Each logger has a list of handlers; in this case, there's just one element in each list. Additionally, we've specified the logging level for each logger.

Here's how we can load this configuration file:

import logging.config 
import yaml 
config_dict = yaml.load(config) logging.config.dictConfig(config_dict)

We parsed the YAML text into dict and then used the dictConfig() function to configure the logging with the given dictionary. Here are some examples of getting loggers and writing messages:

verbose = logging.getLogger("verbose.example.SomeClass") 
audit = logging.getLogger("audit.example.SomeClass") 
verbose.info("Verbose information") 
audit.info("Audit record with before and after state") 

We created two Logger objects; one under the verbose family tree and the other under the audit family tree. When we write to the verbose logger, we'll see the output on the console. When we write to the audit logger, however, we'll see nothing on the console; the record will go to the file that is named in the configuration.

When we look at the logging.handlers module, we see a large number of handlers that we can leverage. By default, the logging module uses old-style % style formatting specifications. These are not like the format specifications for the str.format() method. When we defined our formatter parameters, we used { style formatting, which is consistent with str.format().

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset