Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 391 – Dictionary-Based Configuration For Logging

Author:
Vinay Sajip <vinay_sajip at red-dove.com>
Status:
Final
Type:
Standards Track
Created:
15-Oct-2009
Python-Version:
2.7, 3.2
Post-History:


Table of Contents

Abstract

This PEP describes a new way of configuring logging using a dictionaryto hold configuration information.

Rationale

The present means for configuring Python’s logging package is eitherby using the logging API to configure logging programmatically, orelse by means of ConfigParser-based configuration files.

Programmatic configuration, while offering maximal control, fixes theconfiguration in Python code. This does not facilitate changing iteasily at runtime, and, as a result, the ability to flexibly turn theverbosity of logging up and down for different parts of a usingapplication is lost. This limits the usability of logging as an aidto diagnosing problems - and sometimes, logging is the only diagnosticaid available in production environments.

The ConfigParser-based configuration system is usable, but does notallow its users to configure all aspects of the logging package. Forexample, Filters cannot be configured using this system. Furthermore,the ConfigParser format appears to engender dislike (sometimes strongdislike) in some quarters. Though it was chosen because it was theonly configuration format supported in the Python standard at thattime, many people regard it (or perhaps just the particular schemachosen for logging’s configuration) as ‘crufty’ or ‘ugly’, in somecases apparently on purely aesthetic grounds.

Recent versions of Python include JSON support in the standardlibrary, and this is also usable as a configuration format. In otherenvironments, such as Google App Engine, YAML is used to configureapplications, and usually the configuration of logging would beconsidered an integral part of the application configuration.Although the standard library does not contain YAML support atpresent, support for both JSON and YAML can be provided in a commonway because both of these serialization formats allow deserializationto Python dictionaries.

By providing a way to configure logging by passing the configurationin a dictionary, logging will be easier to configure not only forusers of JSON and/or YAML, but also for users of custom configurationmethods, by providing a common format in which to describe the desiredconfiguration.

Another drawback of the current ConfigParser-based configurationsystem is that it does not support incremental configuration: a newconfiguration completely replaces the existing configuration.Although full flexibility for incremental configuration is difficultto provide in a multi-threaded environment, the new configurationmechanism will allow the provision of limited support for incrementalconfiguration.

Specification

The specification consists of two parts: the API and the format of thedictionary used to convey configuration information (i.e. the schemato which it must conform).

Naming

Historically, the logging package has not beenPEP 8 conformant.At some future time, this will be corrected by changing method andfunction names in the package in order to conform withPEP 8.However, in the interests of uniformity, the proposed additions to theAPI use a naming scheme which is consistent with the present schemeused by logging.

API

The logging.config module will have the following addition:

  • A function, calleddictConfig(), which takes a single argument- the dictionary holding the configuration. Exceptions will beraised if there are errors while processing the dictionary.

It will be possible to customize this API - see the section onAPICustomization.Incremental configuration is covered in its ownsection.

Dictionary Schema - Overview

Before describing the schema in detail, it is worth saying a few wordsabout object connections, support for user-defined objects and accessto external and internal objects.

Object connections

The schema is intended to describe a set of logging objects - loggers,handlers, formatters, filters - which are connected to each other inan object graph. Thus, the schema needs to represent connectionsbetween the objects. For example, say that, once configured, aparticular logger has attached to it a particular handler. For thepurposes of this discussion, we can say that the logger represents thesource, and the handler the destination, of a connection between thetwo. Of course in the configured objects this is represented by thelogger holding a reference to the handler. In the configuration dict,this is done by giving each destination object an id which identifiesit unambiguously, and then using the id in the source object’sconfiguration to indicate that a connection exists between the sourceand the destination object with that id.

So, for example, consider the following YAML snippet:

formatters:brief:# configuration for formatter with id 'brief' goes hereprecise:# configuration for formatter with id 'precise' goes herehandlers:h1:#This is an id# configuration of handler with id 'h1' goes hereformatter:briefh2:#This is another id# configuration of handler with id 'h2' goes hereformatter:preciseloggers:foo.bar.baz:# other configuration for logger 'foo.bar.baz'handlers:[h1,h2]

(Note: YAML will be used in this document as it is a little morereadable than the equivalent Python source form for the dictionary.)

The ids for loggers are the logger names which would be usedprogrammatically to obtain a reference to those loggers, e.g.foo.bar.baz. The ids for Formatters and Filters can be any stringvalue (such asbrief,precise above) and they are transient,in that they are only meaningful for processing the configurationdictionary and used to determine connections between objects, and arenot persisted anywhere when the configuration call is complete.

Handler ids are treated specially, see the section onHandler Ids, below.

The above snippet indicates that logger namedfoo.bar.baz shouldhave two handlers attached to it, which are described by the handleridsh1 andh2. The formatter forh1 is that described by idbrief, and the formatter forh2 is that described by idprecise.

User-defined objects

The schema should support user-defined objects for handlers, filtersand formatters. (Loggers do not need to have different types fordifferent instances, so there is no support - in the configuration -for user-defined logger classes.)

Objects to be configured will typically be described by dictionarieswhich detail their configuration. In some places, the logging systemwill be able to infer from the context how an object is to beinstantiated, but when a user-defined object is to be instantiated,the system will not know how to do this. In order to provide completeflexibility for user-defined object instantiation, the user will needto provide a ‘factory’ - a callable which is called with aconfiguration dictionary and which returns the instantiated object.This will be signalled by an absolute import path to the factory beingmade available under the special key'()'. Here’s a concreteexample:

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.customFormatterFactorybar:bazspam:99.9answer:42

The above YAML snippet defines three formatters. The first, with idbrief, is a standardlogging.Formatter instance with thespecified format string. The second, with iddefault, has alonger format and also defines the time format explicitly, and willresult in alogging.Formatter initialized with those two formatstrings. Shown in Python source form, thebrief anddefaultformatters 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,standardlogging.Formatter instances are created. Theconfiguration sub-dictionary for the third formatter, with idcustom, is:

{'()':'my.package.customFormatterFactory','bar':'baz','spam':99.9,'answer':42}

and this contains the special key'()', which means thatuser-defined instantiation is wanted. In this case, the specifiedfactory callable will be used. If it is an actual callable it will beused directly - otherwise, if you specify a string (as in the example)the actual callable will be located using normal import mechanisms.The callable will be called with theremaining items in theconfiguration sub-dictionary as keyword arguments. In the aboveexample, the formatter with idcustom will be assumed to bereturned 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 avalid keyword parameter name, and so will not clash with the names ofthe keyword arguments used in the call. The'()' also serves as amnemonic that the corresponding value is a callable.

Access to external objects

There are times where a configuration will need to refer to objectsexternal to the configuration, for examplesys.stderr. If theconfiguration dict is constructed using Python code then this isstraightforward, but a problem arises when the configuration isprovided via a text file (e.g. JSON, YAML). In a text file, there isno standard way to distinguishsys.stderr from the literal string'sys.stderr'. To facilitate this distinction, the configurationsystem will look for certain special prefixes in string values andtreat them specially. For example, if the literal string'ext://sys.stderr' is provided as a value in the configuration,then theext:// will be stripped off and the remainder of thevalue processed using normal import mechanisms.

The handling of such prefixes will be done in a way analogous toprotocol handling: there will be a generic mechanism to look forprefixes which match the regular expression^(?P<prefix>[a-z]+)://(?P<suffix>.*)$ whereby, if theprefixis recognised, thesuffix is processed in a prefix-dependentmanner and the result of the processing replaces the string value. Ifthe prefix is not recognised, then the string value will be leftas-is.

The implementation will provide for a set of standard prefixes such asext:// but it will be possible to disable the mechanism completelyor provide additional or different prefixes for special handling.

Access to internal objects

As well as external objects, there is sometimes also a need to referto objects in the configuration. This will be done implicitly by theconfiguration system for things that it knows about. For example, thestring value'DEBUG' for alevel in a logger or handler willautomatically be converted to the valuelogging.DEBUG, and thehandlers,filters andformatter entries will take anobject id and resolve to the appropriate destination object.

However, a more generic mechanism needs to be provided for the caseof user-defined objects which are not known to logging. For example,take the instance oflogging.handlers.MemoryHandler, which takesatarget which is another handler to delegate to. Since the systemalready knows about this class, then in the configuration, the giventarget just needs to be the object id of the relevant targethandler, and the system will resolve to the handler from the id. If,however, a user defines amy.package.MyHandler which has aalternate handler, the configuration system would not know thatthealternate referred to a handler. To cater for this, ageneric resolution system will be provided which allows the user tospecify:

handlers:file:# configuration of file handler goes herecustom:():my.package.MyHandleralternate:cfg://handlers.file

The literal string'cfg://handlers.file' will be resolved in ananalogous way to the strings with theext:// prefix, but lookingin the configuration itself rather than the import namespace. Themechanism will allow access by dot or by index, in a similar way tothat provided bystr.format. Thus, given the following snippet:

handlers:email:class:logging.handlers.SMTPHandlermailhost:localhostfromaddr:my_app@domain.tldtoaddrs:-support_team@domain.tld-dev_team@domain.tldsubject:Houston,wehaveaproblem.

in the configuration, the string'cfg://handlers' would resolve tothe dict with keyhandlers, the string'cfg://handlers.emailwould resolve to the dict with keyemail in thehandlers dict,and so on. The string'cfg://handlers.email.toaddrs[1] wouldresolve to'dev_team.domain.tld' and the string'cfg://handlers.email.toaddrs[0]' would resolve to the value'support_team@domain.tld'. Thesubject value could be accessedusing either'cfg://handlers.email.subject' or, equivalently,'cfg://handlers.email[subject]'. The latter form only needs to beused if the key contains spaces or non-alphanumeric characters. If anindex value consists only of decimal digits, access will be attemptedusing the corresponding integer value, falling back to the stringvalue if needed.

Given a stringcfg://handlers.myhandler.mykey.123, this willresolve toconfig_dict['handlers']['myhandler']['mykey']['123'].If the string is specified ascfg://handlers.myhandler.mykey[123],the system will attempt to retrieve the value fromconfig_dict['handlers']['myhandler']['mykey'][123], and fall backtoconfig_dict['handlers']['myhandler']['mykey']['123'] if thatfails.

Handler Ids

Some specific logging configurations require the use of handler levelsto achieve the desired effect. However, unlike loggers which canalways be identified by their names, handlers have no persistenthandles whereby levels can be changed via an incremental configurationcall.

Therefore, this PEP proposes to add an optionalname property tohandlers. If used, this will add an entry in a dictionary which mapsthe name to the handler. (The entry will be removed when the handleris closed.) When an incremental configuration call is made, handlerswill be looked up in this dictionary to set the handler levelaccording to the value in the configuration. See the section onincremental configuration for more details.

In theory, such a “persistent name” facility could also be providedfor Filters and Formatters. However, there is not a strong case to bemade for being able to configure these incrementally. On the basisthat practicality beats purity, only Handlers will be given this newname property. The id of a handler in the configuration willbecome itsname.

The handler name lookup dictionary is for configuration use only andwill not become part of the public API for the package.

Dictionary Schema - Detail

The dictionary passed todictConfig() must contain the followingkeys:

  • version - to be set to an integer value representing the schemaversion. The only valid value at present is 1, but having this keyallows the schema to evolve while still preserving backwardscompatibility.

All other keys are optional, but if present they will be interpretedas described below. In all cases below where a ‘configuring dict’ ismentioned, it will be checked for the special'()' key to see if acustom instantiation is required. If so, the mechanism describedabove is used to instantiate; otherwise, the context is used todetermine how to instantiate.

  • formatters - the corresponding value will be a dict in which eachkey is a formatter id and each value is a dict describing how toconfigure the corresponding Formatter instance.

    The configuring dict is searched for keysformat anddatefmt(with defaults ofNone) and these are used to construct alogging.Formatter instance.

  • filters - the corresponding value will be a dict in which each keyis a filter id and each value is a dict describing how to configurethe corresponding Filter instance.

    The configuring dict is searched for keyname (defaulting to theempty string) and this is used to construct alogging.Filterinstance.

  • handlers - the corresponding value will be a dict in which eachkey is a handler id and each value is a dict describing how toconfigure the corresponding Handler instance.

    The configuring dict is searched for the following keys:

    • class (mandatory). This is the fully qualified name of thehandler class.
    • level (optional). The level of the handler.
    • formatter (optional). The id of the formatter for thishandler.
    • filters (optional). A list of ids of the filters for thishandler.

    Allother keys are passed through as keyword arguments to thehandler’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 idconsole is instantiated as alogging.StreamHandler, usingsys.stdout as the underlyingstream. The handler with idfile is instantiated as alogging.handlers.RotatingFileHandler with the keyword argumentsfilename='logconfig.log',maxBytes=1024,backupCount=3.

  • loggers - the corresponding value will be a dict in which each keyis a logger name and each value is a dict describing how toconfigure 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 thislogger.
    • handlers (optional). A list of ids of the handlers for thislogger.

    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, exceptthat thepropagate setting will not be applicable.
  • incremental - whether the configuration is to be interpreted asincremental to the existing configuration. This value defaults toFalse, which means that the specified configuration replaces theexisting configuration with the same semantics as used by theexistingfileConfig() API.

    If the specified value isTrue, the configuration is processedas described in the section onIncremental Configuration, below.

  • disable_existing_loggers - whether any existing loggers are to bedisabled. This setting mirrors the parameter of the same name infileConfig(). If absent, this parameter defaults toTrue.This value is ignored ifincremental isTrue.

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: foohandlers:  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: WARNINGroot:  level     : DEBUG  handlers  : [console, file]

Incremental Configuration

It is difficult to provide complete flexibility for incrementalconfiguration. For example, because objects such as filtersand formatters are anonymous, once a configuration is set up, it isnot possible to refer to such anonymous objects when augmenting aconfiguration.

Furthermore, there is not a compelling case for arbitrarily alteringthe object graph of loggers, handlers, filters, formatters atrun-time, once a configuration is set up; the verbosity of loggers andhandlers can be controlled just by setting levels (and, in the case ofloggers, propagation flags). Changing the object graph arbitrarily ina safe way is problematic in a multi-threaded environment; while notimpossible, the benefits are not worth the complexity it adds to theimplementation.

Thus, when theincremental key of a configuration dict is presentand isTrue, the system will ignore anyformatters andfilters entries completely, and process only thelevelsettings in thehandlers entries, and thelevel andpropagate settings in theloggers androot entries.

It’s certainly possible to provide incremental configuration by othermeans, for example makingdictConfig() take anincrementalkeyword argument which defaults toFalse. The reason forsuggesting that a value in the configuration dict be used is that itallows for configurations to be sent over the wire as pickled dictsto a socket listener. Thus, the logging verbosity of a long-runningapplication can be altered over time with no need to stop andrestart the application.

Note: Feedback on incremental configuration needs based on yourpractical experience will be particularly welcome.

API Customization

The bare-bonesdictConfig() API will not be sufficient for alluse cases. Provision for customization of the API will be made byproviding the following:

  • A class, calledDictConfigurator, whose constructor is passedthe dictionary used for configuration, and which has aconfigure() method.
  • A callable, calleddictConfigClass, which will (by default) beset toDictConfigurator. This is provided so that if desired,DictConfigurator can be replaced with a suitable user-definedimplementation.

ThedictConfig() function will calldictConfigClass passingthe specified dictionary, and then call theconfigure() method onthe returned object to actually put the configuration into effect:

defdictConfig(config):dictConfigClass(config).configure()

This should cater to all customization needs. For example, a subclassofDictConfigurator could callDictConfigurator.__init__() inits own__init__(), then set up custom prefixes which would beusable in the subsequentconfigure()call. ThedictConfigClasswould be bound to the subclass, and thendictConfig() could becalled exactly as in the default, uncustomized state.

Change to Socket Listener Implementation

The existing socket listener implementation will be modified asfollows: when a configuration message is received, an attempt will bemade to deserialize to a dictionary using the json module. If thisstep fails, the message will be assumed to be in the fileConfig formatand processed as before. If deserialization is successful, thendictConfig() will be called to process the resulting dictionary.

Configuration Errors

If an error is encountered during configuration, the system will raiseaValueError,TypeError,AttributeError orImportErrorwith a suitably descriptive message. The following is a (possiblyincomplete) list of conditions which will raise an error:

  • Alevel which is not a string or which is a string notcorresponding to an actual logging level
  • Apropagate value which is not a boolean
  • An id which does not have a corresponding destination
  • A non-existent handler id found during an incremental call
  • An invalid logger name
  • Inability to resolve to an internal or external object

Discussion in the community

The PEP has been announced on python-dev and python-list. While therehasn’t been a huge amount of discussion, this is perhaps to beexpected for a niche topic.

Discussion threads on python-dev:

https://mail.python.org/pipermail/python-dev/2009-October/092695.htmlhttps://mail.python.org/pipermail/python-dev/2009-October/092782.htmlhttps://mail.python.org/pipermail/python-dev/2009-October/093062.html

And on python-list:

https://mail.python.org/pipermail/python-list/2009-October/1223658.htmlhttps://mail.python.org/pipermail/python-list/2009-October/1224228.html

There have been some comments in favour of the proposal, noobjections to the proposal as a whole, and some questions andobjections about specific details. These are believed by the authorto have been addressed by making changes to the PEP.

Reference implementation

A reference implementation of the changes is available as a moduledictconfig.py with accompanying unit tests in test_dictconfig.py, at:

http://bitbucket.org/vinay.sajip/dictconfig

This incorporates all features other than the socket listener change.

Copyright

This document has been placed in the public domain.


Source:https://github.com/python/peps/blob/main/peps/pep-0391.rst

Last modified:2025-02-01 08:59:27 GMT


[8]ページ先頭

©2009-2025 Movatter.jp