Add PEP 391: logging configuration using dictionaries.
This commit is contained in:
parent
ab27818ed8
commit
00888ff3be
|
@ -0,0 +1,461 @@
|
|||
PEP: 391
|
||||
Title: Dictionary-Based Configuration For Logging
|
||||
Version: $Revision$
|
||||
Last-Modified: $Date$
|
||||
Author: Vinay Sajip <vinay_sajip at red-dove.com>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 15-Oct-2009
|
||||
Python-Version: 2.7, 3.2
|
||||
Post-History:
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This PEP describes a new way of configuring logging using a dictionary to hold
|
||||
configuration information.
|
||||
|
||||
Rationale
|
||||
=========
|
||||
|
||||
The present means for configuring Python's logging package is either by using
|
||||
the logging API to configure logging programmatically, or else by means of
|
||||
ConfigParser-based configuration files.
|
||||
|
||||
Programmatic configuration, while offering maximal control, fixes the
|
||||
configuration in Python code. This does not facilitate changing it easily at
|
||||
runtime, and, as a result, the ability to flexibly turn the verbosity of
|
||||
logging up and down for different parts of a using application is lost. This
|
||||
limits the usability of logging as an aid to diagnosing problems - and
|
||||
sometimes, logging is the only diagnostic aid available in production
|
||||
environments.
|
||||
|
||||
The ConfigParser-based configuration system is usable, but does not allow its
|
||||
users to configure all aspects of the logging package. For example, Filters
|
||||
cannot be configured using this system. Furthermore, the ConfigParser format
|
||||
appears to engender dislike (sometimes strong dislike) in some quarters.
|
||||
Though it was chosen because it was the only configuration format supported in
|
||||
the Python standard at that time, many people regard it (or perhaps just the
|
||||
particular schema chosen for logging's configuration) as 'crufty' or 'ugly',
|
||||
in some cases apparently on purely aesthetic grounds.
|
||||
|
||||
Recent versions of Python inlude JSON support in the standard library, and
|
||||
this is also usable as a configuration format. In other environments, such as
|
||||
Google App Engine, YAML is used to configure applications, and usually the
|
||||
configuration of logging would be considered an integral part of the
|
||||
application configuration. Although the standard library does not contain
|
||||
YAML support at present, support for both JSON and YAML can be provided in a
|
||||
common way because both of these serialization formats allow deserialization
|
||||
of Python dictionaries.
|
||||
|
||||
By providing a way to configure logging by passing the configuration in a
|
||||
dictionary, logging will be easier to configure not only for users of JSON
|
||||
and/or YAML, but also for users of bespoke configuration methods, by providing
|
||||
a common format in which to describe the desired configuration.
|
||||
|
||||
Another drawback of the current ConfigParser-based configuration system is
|
||||
that it does not support incremental configuration: a new configuration
|
||||
completely replaces the existing configuration. Although full flexibility for
|
||||
incremental configuration is difficult to provide in a multi-threaded
|
||||
environment, the new configuration mechanism will allow the provision of
|
||||
limited support for incremental configuration.
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
The specification consists of two parts: the API and the format of the
|
||||
dictionary used to convey configuration information (i.e. the schema to which
|
||||
it must conform).
|
||||
|
||||
Naming
|
||||
------
|
||||
|
||||
Historically, the logging package has not been PEP-8 conformant. At some
|
||||
future time, this will be corrected by changing method and function names in
|
||||
the package in order to conform with PEP-8. However, in the interests of
|
||||
uniformity, the proposed additions to the API use a naming scheme which is
|
||||
consistent with the present scheme used by logging.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
The logging.config module will have the following additions:
|
||||
|
||||
* A class, called ``DictConfigurator``, whose constructor is passed the
|
||||
dictionary used for configuration, and which has a ``configure()`` method.
|
||||
|
||||
* A callable, called ``dictConfigClass``, which will (by default) be set to
|
||||
``DictConfigurator``. This is provided so that if desired,
|
||||
``DictConfigurator`` can be replaced with a suitable user-defined
|
||||
implementation.
|
||||
|
||||
* A function, called ``dictConfig()``, which takes a single argument - the
|
||||
dictionary holding the configuration. This function will call
|
||||
``dictConfigClass`` passing the specified dictionary, and then call the
|
||||
``configure()`` method on the returned object to actually put the
|
||||
configuration into effect::
|
||||
|
||||
def dictConfig(config):
|
||||
dictConfigClass(config).configure()
|
||||
|
||||
Dictionary Schema - Overview
|
||||
----------------------------
|
||||
|
||||
Before describing the schema in detail, it is worth saying a few words about
|
||||
object connections, support for user-defined objects and access to external
|
||||
objects.
|
||||
|
||||
Object connections
|
||||
''''''''''''''''''
|
||||
|
||||
The schema is intended to describe a set of logging objects - loggers,
|
||||
handlers, formatters, filters - which are connected to each other in an
|
||||
object graph. Thus, the schema needs to represent connections between the
|
||||
objects. For example, say that, once configured, a particular logger has an
|
||||
attached to it a particular handler. For the purposes of this discussion,
|
||||
we can say that the logger represents the source, and the handler the
|
||||
destination, of a connection between the two. Of course in the configured
|
||||
objects this is represented by the logger holding a reference to the
|
||||
handler. In the configuration dict, this is done by giving each destination
|
||||
object an id which identifies it unambiguously, and then using the id in the
|
||||
source object's configuration to indicate that a connection exists between
|
||||
the source and the destination object with that id.
|
||||
|
||||
So, for example, consider the following YAML snippet::
|
||||
|
||||
handlers:
|
||||
h1: #This is an id
|
||||
# configuration of handler with id h1 goes here
|
||||
h2: #This is another id
|
||||
# configuration of handler with id h2 goes here
|
||||
loggers:
|
||||
foo.bar.baz:
|
||||
# other configuration for logger "foo.bar.baz"
|
||||
handlers: [h1, h2]
|
||||
|
||||
(Note: YAML will be used in this document as it is more readable than the
|
||||
equivalent Python source form for the dictionary.)
|
||||
|
||||
The ids for loggers are the logger names which would be used
|
||||
programmatically to obtain a reference to those loggers, e.g.
|
||||
``foo.bar.baz``. The ids for other objects can be any string value (such as
|
||||
``h1``, ``h2`` above) and they are transient, in that they are only
|
||||
meaningful for processing the configuration dictionary and used to
|
||||
determine connections between objects, and are not persisted anywhere when
|
||||
the configuration call is complete.
|
||||
|
||||
The above snippet indicates that logger named ``foo.bar.baz`` should have
|
||||
two handlers attached to it, which are described by the handler ids ``h1``
|
||||
and ``h2``.
|
||||
|
||||
User-defined objects
|
||||
''''''''''''''''''''
|
||||
|
||||
The schema should support user-defined objects for handlers, filters and
|
||||
formatters. (Loggers do not need to have different types for different
|
||||
instances, so there is no support - in the configuration - for user-defined
|
||||
logger classes.)
|
||||
|
||||
Objects to be configured will typically be described by dictionaries which
|
||||
detail their configuration. In some places, the logging system will be able
|
||||
to infer from the context how an object is to be instantiated, but when a
|
||||
user-defined object is to be instantiated, the system will not know how to do
|
||||
this. In order to provide complete flexibility for user-defined object
|
||||
instantiation, the user will need to provide a 'factory' - a callable which
|
||||
is called with a configuration dictionary and which returns the instantiated
|
||||
object. This will be signalled by the factory being made available under
|
||||
the special key ``'()'``. Here's a concrete example::
|
||||
|
||||
formatters:
|
||||
brief:
|
||||
format: '%(message)s'
|
||||
default:
|
||||
format: '%(asctime)s %(levelname)-8s %(name)-15s %(message)s'
|
||||
datefmt: '%Y-%m-%d %H:%M:%S'
|
||||
custom:
|
||||
(): my.package.customFormatterFactory
|
||||
bar: baz
|
||||
spam: 99.9
|
||||
answer: 42
|
||||
|
||||
The above YAML snippet defines three formatters. The first, with id
|
||||
``brief``, is a standard ``logging.Formatter`` instance with the
|
||||
specified format string. The second, with id ``default``, has a longer
|
||||
format and also defines the time format explicitly, and will result in a
|
||||
``logging.Formatter`` initialized with those two format strings. Shown in
|
||||
Python source form, the ``brief`` and ``default`` formatters have
|
||||
have configuration sub-dictionaries::
|
||||
|
||||
{
|
||||
'format' : '%(message)s'
|
||||
}
|
||||
|
||||
and::
|
||||
|
||||
{
|
||||
'format' : '%(asctime)s %(levelname)-8s %(name)-15s %(message)s',
|
||||
'datefmt' : '%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
|
||||
respectively, and as these dictionaries do not contain the special key
|
||||
``'()'``, the instantiation is inferred from the context: as a result,
|
||||
standard ``logging.Formatter`` instances are created. The configuration
|
||||
sub-dictionary for the third formatter, with id ``custom``, is::
|
||||
|
||||
{
|
||||
'()' : 'my.package.customFormatterFactory',
|
||||
'bar' : 'baz',
|
||||
'spam' : 99.9,
|
||||
'answer' : 42
|
||||
}
|
||||
|
||||
and this contains the special key ``'()'``, which means that user-defined
|
||||
instantiation is wanted. In this case, the specified factory callable will be
|
||||
located using normal import mechanisms and called with the *remaining* items
|
||||
in the configuration sub-dictionary as keyword arguments. In the above
|
||||
example, the formatter with id ``custom`` will be assumed to be returned by
|
||||
the call::
|
||||
|
||||
my.package.customFormatterFactory(bar="baz", spam=99.9, answer=42)
|
||||
|
||||
The key ``'()'`` has been used as the special key because it is not a valid
|
||||
keyword parameter name, and so will not clash with the names of the keyword
|
||||
arguments used in the call. The ``'()'`` also serves as a mnemonic that the
|
||||
corresponding value is a callable.
|
||||
|
||||
Access to external objects
|
||||
''''''''''''''''''''''''''
|
||||
|
||||
There are times where a configuration will need to refer to objects external
|
||||
to the configuration, for example ``sys.stderr``. If the configuration dict
|
||||
is constructed using Python code then this is straightforward, but a problem
|
||||
arises when the configuration is provided via a text file (e.g. JSON, YAML).
|
||||
In a text file, there is no standard way to distinguish ``sys.stderr`` from
|
||||
the literal string ``'sys.stderr'``. To facilitate this distinction, the
|
||||
configuration system will look for certain special prefixes in string values
|
||||
and treat them specially. For example, if the literal string
|
||||
``'ext://sys.stderr'`` is provided as a value in the configuration, then the
|
||||
``ext://`` will be stripped off and the remainder of the value processed using
|
||||
normal import mechanisms.
|
||||
|
||||
The handling of such prefixes will be done in a way analogous to protocol
|
||||
handling: there will be a generic mechanism to look for prefixes which match
|
||||
the regular expression ``^(?P<prefix>[a-z]+)://(?P<suffix>.*)$`` whereby, if
|
||||
the ``prefix`` is recognised, the ``suffix`` is processed in a
|
||||
prefix-dependent manner and the result of the processing replaces the string
|
||||
value. If the prefix is not recognised, then the string value will be left
|
||||
as-is.
|
||||
|
||||
The implementation will provide for a set of standard prefixes such as
|
||||
``ext://`` but it will be possible to disable the mechanism completely or
|
||||
provide additional or different prefixes for special handling.
|
||||
|
||||
Dictionary Schema - Detail
|
||||
--------------------------
|
||||
|
||||
The dictionary passed to ``dictConfig()`` must contain the following keys:
|
||||
|
||||
* `version` - to be set to an integer value representing the schema
|
||||
version. The only valid value at present is 1, but having this key allows
|
||||
the schema to evolve while still preserving backwards compatibility.
|
||||
|
||||
All other keys are optional, but if present they will be interpreted as described
|
||||
below. In all cases below where a 'configuring dict' is mentioned, it will be
|
||||
checked for the special ``'()'`` key to see if a custom instantiation is
|
||||
required. If so, the mechanism described above is used to instantiate;
|
||||
otherwise, the context is used to determine how to instantiate.
|
||||
|
||||
* `formatters` - the corresponding value will be a dict in which each key is
|
||||
a formatter id and each value is a dict describing how to configure the
|
||||
corresponding Formatter instance.
|
||||
|
||||
The configuring dict is searched for keys ``format`` and ``datefmt`` (with
|
||||
defaults of ``None``) and these are used to construct a
|
||||
``logging.Formatter`` instance.
|
||||
|
||||
* `filters` - the corresponding value will be a dict in which each key is
|
||||
a filter id and each value is a dict describing how to configure the
|
||||
corresponding Filter instance.
|
||||
|
||||
The configuring dict is searched for key ``name`` (defaulting to the empty
|
||||
string) and this is used to construct a ``logging.Filter`` instance.
|
||||
|
||||
* `handlers` - the corresponding value will be a dict in which each key is
|
||||
a handler id and each value is a dict describing how to configure the
|
||||
corresponding Handler instance.
|
||||
|
||||
The configuring dict is searched for the following keys:
|
||||
|
||||
* ``class`` (mandatory). This is the fully qualified name of the handler
|
||||
class.
|
||||
|
||||
* ``level`` (optional). The level of the handler.
|
||||
|
||||
* ``formatter`` (optional). The id of the formatter for this handler.
|
||||
|
||||
* ``filters`` (optional). A list of ids of the filters for this handler.
|
||||
|
||||
All *other* keys are passed through as keyword arguments to the handler's
|
||||
constructor. For example, given the snippet::
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class : logging.StreamHandler
|
||||
formatter: brief
|
||||
level : INFO
|
||||
filters: [allow_foo]
|
||||
stream : ext://sys.stdout
|
||||
file:
|
||||
class : logging.handlers.RotatingFileHandler
|
||||
formatter: precise
|
||||
filename: logconfig.log
|
||||
maxBytes: 1024
|
||||
backupCount: 3
|
||||
|
||||
the handler with id ``console`` is instantiated as a
|
||||
``logging.StreamHandler``, using ``sys.stdout`` as the underlying stream.
|
||||
The handler with id ``file`` is instantiated as a
|
||||
``logging.handlers.RotatingFileHandler`` with the keyword arguments
|
||||
``filename="logconfig.log", maxBytes=1024, backupCount=3``.
|
||||
|
||||
* `loggers` - the corresponding value will be a dict in which each key is
|
||||
a logger name and each value is a dict describing how to configure the
|
||||
corresponding Logger instance.
|
||||
|
||||
The configuring dict is searched for the following keys:
|
||||
|
||||
* ``level`` (optional). The level of the logger.
|
||||
|
||||
* ``propagate`` (optional). The propagation setting of the logger.
|
||||
|
||||
* ``filters`` (optional). A list of ids of the filters for this logger.
|
||||
|
||||
* ``handlers`` (optional). A list of ids of the handlers for this logger.
|
||||
|
||||
The specified loggers will be configured according to the level,
|
||||
propagation, filters and handlers specified.
|
||||
|
||||
* `root` - this will be the configuration for the root logger. Processing of
|
||||
the configuration will be as for any logger, except that the ``propagate``
|
||||
setting will not be applicable.
|
||||
|
||||
* `incremental` - whether the configuration is to be interpreted as
|
||||
incremental to the existing configuration. This value defaults to False,
|
||||
which means that the specified configuration replaces the existing
|
||||
configuration with the same semantics as used by the existing
|
||||
``fileConfig()`` API.
|
||||
|
||||
If the specified value is True, the configuration is processed as described
|
||||
in the section on "Incremental Configuration", below.
|
||||
|
||||
A Working Example
|
||||
-----------------
|
||||
|
||||
The following is an actual working configuration in YAML format (except that
|
||||
the email addresses are bogus)::
|
||||
|
||||
formatters:
|
||||
brief:
|
||||
format: '%(levelname)-8s: %(name)-15s: %(message)s'
|
||||
precise:
|
||||
format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s'
|
||||
filters:
|
||||
allow_foo:
|
||||
name: foo
|
||||
handlers:
|
||||
console:
|
||||
class : logging.StreamHandler
|
||||
formatter: brief
|
||||
level : INFO
|
||||
stream : ext://sys.stdout
|
||||
filters: [allow_foo]
|
||||
file:
|
||||
class : logging.handlers.RotatingFileHandler
|
||||
formatter: precise
|
||||
filename: logconfig.log
|
||||
maxBytes: 1024
|
||||
backupCount: 3
|
||||
debugfile:
|
||||
class : logging.FileHandler
|
||||
formatter: precise
|
||||
filename: logconfig-detail.log
|
||||
mode: a
|
||||
email:
|
||||
class: logging.handlers.SMTPHandler
|
||||
mailhost: localhost
|
||||
fromaddr: my_app@domain.tld
|
||||
toaddrs:
|
||||
- support_team@domain.tld
|
||||
- dev_team@domain.tld
|
||||
subject: Houston, we have a problem.
|
||||
loggers:
|
||||
foo:
|
||||
level : ERROR
|
||||
handlers: [debugfile]
|
||||
spam:
|
||||
level : CRITICAL
|
||||
handlers: [debugfile]
|
||||
propagate: no
|
||||
bar.baz:
|
||||
level: WARNING
|
||||
root:
|
||||
level : DEBUG
|
||||
handlers : [console, file]
|
||||
|
||||
Incremental Configuration
|
||||
=========================
|
||||
|
||||
It is difficult to provide complete flexibility for incremental configuration.
|
||||
For example, because objects such as handlers, filters and formatters are
|
||||
anonymous, once a configuration is set up, it is not possible to refer to such
|
||||
anonymous objects when augmenting a configuration. For example, if an initial
|
||||
call is made to configure the system where logger ``foo`` has a handler with
|
||||
id ``console`` attached, then a subsequent call to configure a logger ``bar``
|
||||
with id ``console`` would create a new handler instance, as the id ``console``
|
||||
from the first call isn't kept.
|
||||
|
||||
Furthermore, there is not a compelling case for arbitrarily altering the
|
||||
object graph of loggers, handlers, filters, formatters at run-time, once a
|
||||
configuration is set up; the verbosity of loggers can be controlled just by
|
||||
setting levels (and perhaps propagation flags).
|
||||
|
||||
Thus, when the ``incremental`` key of a configuration dict is present and
|
||||
is ``True``, the system will ignore the ``formatters``, ``filters``,
|
||||
``handlers`` entries completely, and process only the ``level`` and
|
||||
``propagate`` settings in the ``loggers`` and ``root`` entries.
|
||||
|
||||
Configuration Errors
|
||||
====================
|
||||
|
||||
If an error is encountered during configuration, the system will raise a
|
||||
``ValueError`` or a ``TypeError`` with a suitably descriptive message. The
|
||||
following is a (possibly incomplete) list of conditions which will raise an
|
||||
error:
|
||||
|
||||
* A ``level`` which is not a string or which is a string not corresponding to
|
||||
an actual logging level
|
||||
|
||||
* A ``propagate`` value which is not a Boolean
|
||||
|
||||
* An id which does not have a corresponding destination
|
||||
|
||||
* An invalid logger name
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document has been placed in the public domain.
|
||||
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
||||
|
Loading…
Reference in New Issue