2
# Copyright (C) 2009-2013 Vinay Sajip. See LICENSE.txt for details.
4
import logging.handlers
16
StandardError = Exception
18
IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
21
m = IDENTIFIER.match(s)
23
raise ValueError('Not a valid Python identifier: %r' % s)
27
# This function is defined in logging only in recent versions of Python
30
from logging import _checkLevel
32
def _checkLevel(level):
33
if isinstance(level, int):
35
elif str(level) == level:
36
if level not in logging._levelNames:
37
raise ValueError('Unknown level: %r' % level)
38
rv = logging._levelNames[level]
40
raise TypeError('Level not an integer or a '
41
'valid string: %r' % level)
44
# The ConvertingXXX classes are wrappers around standard Python containers,
45
# and they serve to convert any suitable values in the container. The
46
# conversion converts base dicts, lists and tuples to their wrapped
47
# equivalents, whereas strings which match a conversion format are converted
50
# Each wrapper should have a configurator attribute holding the actual
51
# configurator to use for conversion.
53
class ConvertingDict(dict):
54
"""A converting dictionary wrapper."""
56
def __getitem__(self, key):
57
value = dict.__getitem__(self, key)
58
result = self.configurator.convert(value)
59
#If the converted value is different, save for next time
60
if value is not result:
62
if type(result) in (ConvertingDict, ConvertingList,
68
def get(self, key, default=None):
69
value = dict.get(self, key, default)
70
result = self.configurator.convert(value)
71
#If the converted value is different, save for next time
72
if value is not result:
74
if type(result) in (ConvertingDict, ConvertingList,
80
def pop(self, key, default=None):
81
value = dict.pop(self, key, default)
82
result = self.configurator.convert(value)
83
if value is not result:
84
if type(result) in (ConvertingDict, ConvertingList,
90
class ConvertingList(list):
91
"""A converting list wrapper."""
92
def __getitem__(self, key):
93
value = list.__getitem__(self, key)
94
result = self.configurator.convert(value)
95
#If the converted value is different, save for next time
96
if value is not result:
98
if type(result) in (ConvertingDict, ConvertingList,
104
def pop(self, idx=-1):
105
value = list.pop(self, idx)
106
result = self.configurator.convert(value)
107
if value is not result:
108
if type(result) in (ConvertingDict, ConvertingList,
113
class ConvertingTuple(tuple):
114
"""A converting tuple wrapper."""
115
def __getitem__(self, key):
116
value = tuple.__getitem__(self, key)
117
result = self.configurator.convert(value)
118
if value is not result:
119
if type(result) in (ConvertingDict, ConvertingList,
125
class BaseConfigurator(object):
127
The configurator base class which defines some useful defaults.
130
CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
132
WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
133
DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
134
INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
135
DIGIT_PATTERN = re.compile(r'^\d+$')
138
'ext' : 'ext_convert',
139
'cfg' : 'cfg_convert',
142
# We might want to use a different one, e.g. importlib
143
importer = __import__
144
"Allows the importer to be redefined."
146
def __init__(self, config):
148
Initialise an instance with the specified configuration
151
self.config = ConvertingDict(config)
152
self.config.configurator = self
154
def resolve(self, s):
156
Resolve strings to objects using standard import and attribute
162
found = self.importer(used)
166
found = getattr(found, frag)
167
except AttributeError:
169
found = getattr(found, frag)
172
e, tb = sys.exc_info()[1:]
173
v = ValueError('Cannot resolve %r: %s' % (s, e))
174
v.__cause__, v.__traceback__ = e, tb
177
def ext_convert(self, value):
178
"""Default converter for the ext:// protocol."""
179
return self.resolve(value)
181
def cfg_convert(self, value):
182
"""Default converter for the cfg:// protocol."""
184
m = self.WORD_PATTERN.match(rest)
186
raise ValueError("Unable to convert %r" % value)
188
rest = rest[m.end():]
189
d = self.config[m.groups()[0]]
192
m = self.DOT_PATTERN.match(rest)
196
m = self.INDEX_PATTERN.match(rest)
199
if not self.DIGIT_PATTERN.match(idx):
203
n = int(idx) # try as number first (most likely)
208
rest = rest[m.end():]
210
raise ValueError('Unable to convert '
211
'%r at %r' % (value, rest))
212
#rest should be empty
215
def convert(self, value):
217
Convert values to an appropriate type. dicts, lists and tuples are
218
replaced by their converting alternatives. Strings are checked to
219
see if they have a conversion format and are converted if they do.
221
if not isinstance(value, ConvertingDict) and isinstance(value, dict):
222
value = ConvertingDict(value)
223
value.configurator = self
224
elif not isinstance(value, ConvertingList) and isinstance(value, list):
225
value = ConvertingList(value)
226
value.configurator = self
227
elif not isinstance(value, ConvertingTuple) and\
228
isinstance(value, tuple):
229
value = ConvertingTuple(value)
230
value.configurator = self
231
elif isinstance(value, basestring):
232
m = self.CONVERT_PATTERN.match(value)
236
converter = self.value_converters.get(prefix, None)
239
converter = getattr(self, converter)
240
value = converter(suffix)
243
def configure_custom(self, config):
244
"""Configure an object with a user-supplied factory."""
246
if isinstance(c, basestring):
248
props = config.pop('.', None)
249
# Check for valid identifiers
250
kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
253
for name, value in props.items():
254
setattr(result, name, value)
257
def as_tuple(self, value):
258
"""Utility function which converts lists to tuples."""
259
if isinstance(value, list):
263
def named_handlers_supported():
264
major, minor = sys.version_info[:2]
273
class DictConfigurator(BaseConfigurator):
275
Configure logging using a dictionary-like object to describe the
280
"""Do the configuration."""
283
if 'version' not in config:
284
raise ValueError("dictionary doesn't specify a version")
285
if config['version'] != 1:
286
raise ValueError("Unsupported version: %s" % config['version'])
287
incremental = config.pop('incremental', False)
289
logging._acquireLock()
292
handlers = config.get('handlers', EMPTY_DICT)
293
# incremental handler config only if handler name
294
# ties in to logging._handlers (Python 2.7, 3.2+)
295
if named_handlers_supported():
296
for name in handlers:
297
if name not in logging._handlers:
298
raise ValueError('No handler found with '
302
handler = logging._handlers[name]
303
handler_config = handlers[name]
304
level = handler_config.get('level', None)
306
handler.setLevel(_checkLevel(level))
307
except StandardError:
308
e = sys.exc_info()[1]
309
raise ValueError('Unable to configure handler '
310
'%r: %s' % (name, e))
311
loggers = config.get('loggers', EMPTY_DICT)
314
self.configure_logger(name, loggers[name], True)
315
except StandardError:
316
e = sys.exc_info()[1]
317
raise ValueError('Unable to configure logger '
318
'%r: %s' % (name, e))
319
root = config.get('root', None)
322
self.configure_root(root, True)
323
except StandardError:
324
e = sys.exc_info()[1]
325
raise ValueError('Unable to configure root '
328
disable_existing = config.pop('disable_existing_loggers', True)
330
logging._handlers.clear()
331
del logging._handlerList[:]
333
# Do formatters first - they don't refer to anything else
334
formatters = config.get('formatters', EMPTY_DICT)
335
for name in formatters:
337
formatters[name] = self.configure_formatter(
339
except StandardError:
340
e = sys.exc_info()[1]
341
raise ValueError('Unable to configure '
342
'formatter %r: %s' % (name, e))
343
# Next, do filters - they don't refer to anything else, either
344
filters = config.get('filters', EMPTY_DICT)
347
filters[name] = self.configure_filter(filters[name])
348
except StandardError:
349
e = sys.exc_info()[1]
350
raise ValueError('Unable to configure '
351
'filter %r: %s' % (name, e))
353
# Next, do handlers - they refer to formatters and filters
354
# As handlers can refer to other handlers, sort the keys
355
# to allow a deterministic order of configuration
356
handlers = config.get('handlers', EMPTY_DICT)
357
for name in sorted(handlers):
359
handler = self.configure_handler(handlers[name])
361
handlers[name] = handler
362
except StandardError:
363
e = sys.exc_info()[1]
364
raise ValueError('Unable to configure handler '
365
'%r: %s' % (name, e))
366
# Next, do loggers - they refer to handlers and filters
368
#we don't want to lose the existing loggers,
369
#since other threads may have pointers to them.
370
#existing is set to contain all existing loggers,
371
#and as we go through the new configuration we
372
#remove any which are configured. At the end,
373
#what's left in existing is the set of loggers
374
#which were in the previous configuration but
375
#which are not in the new configuration.
377
existing = sorted(root.manager.loggerDict.keys())
378
#The list needs to be sorted so that we can
379
#avoid disabling child loggers of explicitly
380
#named loggers. With a sorted list it is easier
381
#to find the child loggers.
382
#We'll keep the list of existing loggers
383
#which are children of named loggers here...
385
#now set up the new ones...
386
loggers = config.get('loggers', EMPTY_DICT)
389
i = existing.index(name)
390
prefixed = name + "."
391
pflen = len(prefixed)
392
num_existing = len(existing)
393
i = i + 1 # look at the entry after name
394
while (i < num_existing) and\
395
(existing[i][:pflen] == prefixed):
396
child_loggers.append(existing[i])
398
existing.remove(name)
400
self.configure_logger(name, loggers[name])
401
except StandardError:
402
e = sys.exc_info()[1]
403
raise ValueError('Unable to configure logger '
404
'%r: %s' % (name, e))
406
#Disable any old loggers. There's no point deleting
407
#them as other threads may continue to hold references
408
#and by disabling them, you stop them doing any logging.
409
#However, don't disable children of named loggers, as that's
410
#probably not what was intended by the user.
412
logger = root.manager.loggerDict[log]
413
if log in child_loggers:
414
logger.level = logging.NOTSET
416
logger.propagate = True
417
elif disable_existing:
418
logger.disabled = True
420
# And finally, do the root logger
421
root = config.get('root', None)
424
self.configure_root(root)
425
except StandardError:
426
e = sys.exc_info()[1]
427
raise ValueError('Unable to configure root '
430
logging._releaseLock()
432
def configure_formatter(self, config):
433
"""Configure a formatter from a dictionary."""
435
factory = config['()'] # for use in exception handler
437
result = self.configure_custom(config)
439
te = sys.exc_info()[1]
440
if "'format'" not in str(te):
442
#Name of parameter changed from fmt to format.
443
#Retry with old name.
444
#This is so that code can be used with older Python versions
446
config['fmt'] = config.pop('format')
447
config['()'] = factory
448
result = self.configure_custom(config)
450
fmt = config.get('format', None)
451
dfmt = config.get('datefmt', None)
452
result = logging.Formatter(fmt, dfmt)
455
def configure_filter(self, config):
456
"""Configure a filter from a dictionary."""
458
result = self.configure_custom(config)
460
name = config.get('name', '')
461
result = logging.Filter(name)
464
def add_filters(self, filterer, filters):
465
"""Add filters to a filterer from a list of names."""
468
filterer.addFilter(self.config['filters'][f])
469
except StandardError:
470
e = sys.exc_info()[1]
471
raise ValueError('Unable to add filter %r: %s' % (f, e))
473
def configure_handler(self, config):
474
"""Configure a handler from a dictionary."""
475
formatter = config.pop('formatter', None)
478
formatter = self.config['formatters'][formatter]
479
except StandardError:
480
e = sys.exc_info()[1]
481
raise ValueError('Unable to set formatter '
482
'%r: %s' % (formatter, e))
483
level = config.pop('level', None)
484
filters = config.pop('filters', None)
487
if isinstance(c, basestring):
491
klass = self.resolve(config.pop('class'))
492
#Special case for handler which refers to another handler
493
if issubclass(klass, logging.handlers.MemoryHandler) and\
496
config['target'] = self.config['handlers'][config['target']]
497
except StandardError:
498
e = sys.exc_info()[1]
499
raise ValueError('Unable to set target handler '
500
'%r: %s' % (config['target'], e))
501
elif issubclass(klass, logging.handlers.SMTPHandler) and\
502
'mailhost' in config:
503
config['mailhost'] = self.as_tuple(config['mailhost'])
504
elif issubclass(klass, logging.handlers.SysLogHandler) and\
506
config['address'] = self.as_tuple(config['address'])
508
kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
510
result = factory(**kwargs)
512
te = sys.exc_info()[1]
513
if "'stream'" not in str(te):
515
#The argument name changed from strm to stream
516
#Retry with old name.
517
#This is so that code can be used with older Python versions
519
kwargs['strm'] = kwargs.pop('stream')
520
result = factory(**kwargs)
522
result.setFormatter(formatter)
523
if level is not None:
524
result.setLevel(_checkLevel(level))
526
self.add_filters(result, filters)
529
def add_handlers(self, logger, handlers):
530
"""Add handlers to a logger from a list of names."""
533
logger.addHandler(self.config['handlers'][h])
534
except StandardError:
535
e = sys.exc_info()[1]
536
raise ValueError('Unable to add handler %r: %s' % (h, e))
538
def common_logger_config(self, logger, config, incremental=False):
540
Perform configuration which is common to root and non-root loggers.
542
level = config.get('level', None)
543
if level is not None:
544
logger.setLevel(_checkLevel(level))
546
#Remove any existing handlers
547
for h in logger.handlers[:]:
548
logger.removeHandler(h)
549
handlers = config.get('handlers', None)
551
self.add_handlers(logger, handlers)
552
filters = config.get('filters', None)
554
self.add_filters(logger, filters)
556
def configure_logger(self, name, config, incremental=False):
557
"""Configure a non-root logger from a dictionary."""
558
logger = logging.getLogger(name)
559
self.common_logger_config(logger, config, incremental)
560
propagate = config.get('propagate', None)
561
if propagate is not None:
562
logger.propagate = propagate
564
def configure_root(self, config, incremental=False):
565
"""Configure a root logger from a dictionary."""
566
root = logging.getLogger()
567
self.common_logger_config(root, config, incremental)
569
dictConfigClass = DictConfigurator
571
def dictConfig(config):
572
"""Configure logging using a dictionary."""
573
dictConfigClass(config).configure()