blob: fd3aded7608cbfebcc7002d456aa3b631eec9453 [file] [log] [blame]
Jingwen Chen475b3cc2021-01-05 21:45:16 -05001# Copyright 2001-2019 by Vinay Sajip. All Rights Reserved.
2#
3# Permission to use, copy, modify, and distribute this software and its
4# documentation for any purpose and without fee is hereby granted,
5# provided that the above copyright notice appear in all copies and that
6# both that copyright notice and this permission notice appear in
7# supporting documentation, and that the name of Vinay Sajip
8# not be used in advertising or publicity pertaining to distribution
9# of the software without specific, written prior permission.
10# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
11# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
12# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
13# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
14# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
15# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
17"""
18Configuration functions for the logging package for Python. The core package
19is based on PEP 282 and comments thereto in comp.lang.python, and influenced
20by Apache's log4j system.
21
22Copyright (C) 2001-2019 Vinay Sajip. All Rights Reserved.
23
24To use, simply 'import logging' and log away!
25"""
26
27import errno
28import io
29import logging
30import logging.handlers
31import re
32import struct
33import sys
34import threading
35import traceback
36
37from socketserver import ThreadingTCPServer, StreamRequestHandler
38
39
40DEFAULT_LOGGING_CONFIG_PORT = 9030
41
42RESET_ERROR = errno.ECONNRESET
43
44#
45# The following code implements a socket listener for on-the-fly
46# reconfiguration of logging.
47#
48# _listener holds the server object doing the listening
49_listener = None
50
51def fileConfig(fname, defaults=None, disable_existing_loggers=True):
52 """
53 Read the logging configuration from a ConfigParser-format file.
54
55 This can be called several times from an application, allowing an end user
56 the ability to select from various pre-canned configurations (if the
57 developer provides a mechanism to present the choices and load the chosen
58 configuration).
59 """
60 import configparser
61
62 if isinstance(fname, configparser.RawConfigParser):
63 cp = fname
64 else:
65 cp = configparser.ConfigParser(defaults)
66 if hasattr(fname, 'readline'):
67 cp.read_file(fname)
68 else:
69 cp.read(fname)
70
71 formatters = _create_formatters(cp)
72
73 # critical section
74 logging._acquireLock()
75 try:
76 _clearExistingHandlers()
77
78 # Handlers add themselves to logging._handlers
79 handlers = _install_handlers(cp, formatters)
80 _install_loggers(cp, handlers, disable_existing_loggers)
81 finally:
82 logging._releaseLock()
83
84
85def _resolve(name):
86 """Resolve a dotted name to a global object."""
87 name = name.split('.')
88 used = name.pop(0)
89 found = __import__(used)
90 for n in name:
91 used = used + '.' + n
92 try:
93 found = getattr(found, n)
94 except AttributeError:
95 __import__(used)
96 found = getattr(found, n)
97 return found
98
99def _strip_spaces(alist):
100 return map(str.strip, alist)
101
102def _create_formatters(cp):
103 """Create and return formatters"""
104 flist = cp["formatters"]["keys"]
105 if not len(flist):
106 return {}
107 flist = flist.split(",")
108 flist = _strip_spaces(flist)
109 formatters = {}
110 for form in flist:
111 sectname = "formatter_%s" % form
112 fs = cp.get(sectname, "format", raw=True, fallback=None)
113 dfs = cp.get(sectname, "datefmt", raw=True, fallback=None)
114 stl = cp.get(sectname, "style", raw=True, fallback='%')
115 c = logging.Formatter
116 class_name = cp[sectname].get("class")
117 if class_name:
118 c = _resolve(class_name)
119 f = c(fs, dfs, stl)
120 formatters[form] = f
121 return formatters
122
123
124def _install_handlers(cp, formatters):
125 """Install and return handlers"""
126 hlist = cp["handlers"]["keys"]
127 if not len(hlist):
128 return {}
129 hlist = hlist.split(",")
130 hlist = _strip_spaces(hlist)
131 handlers = {}
132 fixups = [] #for inter-handler references
133 for hand in hlist:
134 section = cp["handler_%s" % hand]
135 klass = section["class"]
136 fmt = section.get("formatter", "")
137 try:
138 klass = eval(klass, vars(logging))
139 except (AttributeError, NameError):
140 klass = _resolve(klass)
141 args = section.get("args", '()')
142 args = eval(args, vars(logging))
143 kwargs = section.get("kwargs", '{}')
144 kwargs = eval(kwargs, vars(logging))
145 h = klass(*args, **kwargs)
Elliott Hughes96c2b6b2021-01-26 11:15:15 -0800146 h.name = hand
Jingwen Chen475b3cc2021-01-05 21:45:16 -0500147 if "level" in section:
148 level = section["level"]
149 h.setLevel(level)
150 if len(fmt):
151 h.setFormatter(formatters[fmt])
152 if issubclass(klass, logging.handlers.MemoryHandler):
153 target = section.get("target", "")
154 if len(target): #the target handler may not be loaded yet, so keep for later...
155 fixups.append((h, target))
156 handlers[hand] = h
157 #now all handlers are loaded, fixup inter-handler references...
158 for h, t in fixups:
159 h.setTarget(handlers[t])
160 return handlers
161
162def _handle_existing_loggers(existing, child_loggers, disable_existing):
163 """
164 When (re)configuring logging, handle loggers which were in the previous
165 configuration but are not in the new configuration. There's no point
166 deleting them as other threads may continue to hold references to them;
167 and by disabling them, you stop them doing any logging.
168
169 However, don't disable children of named loggers, as that's probably not
170 what was intended by the user. Also, allow existing loggers to NOT be
171 disabled if disable_existing is false.
172 """
173 root = logging.root
174 for log in existing:
175 logger = root.manager.loggerDict[log]
176 if log in child_loggers:
177 if not isinstance(logger, logging.PlaceHolder):
178 logger.setLevel(logging.NOTSET)
179 logger.handlers = []
180 logger.propagate = True
181 else:
182 logger.disabled = disable_existing
183
184def _install_loggers(cp, handlers, disable_existing):
185 """Create and install loggers"""
186
187 # configure the root first
188 llist = cp["loggers"]["keys"]
189 llist = llist.split(",")
190 llist = list(_strip_spaces(llist))
191 llist.remove("root")
192 section = cp["logger_root"]
193 root = logging.root
194 log = root
195 if "level" in section:
196 level = section["level"]
197 log.setLevel(level)
198 for h in root.handlers[:]:
199 root.removeHandler(h)
200 hlist = section["handlers"]
201 if len(hlist):
202 hlist = hlist.split(",")
203 hlist = _strip_spaces(hlist)
204 for hand in hlist:
205 log.addHandler(handlers[hand])
206
207 #and now the others...
208 #we don't want to lose the existing loggers,
209 #since other threads may have pointers to them.
210 #existing is set to contain all existing loggers,
211 #and as we go through the new configuration we
212 #remove any which are configured. At the end,
213 #what's left in existing is the set of loggers
214 #which were in the previous configuration but
215 #which are not in the new configuration.
216 existing = list(root.manager.loggerDict.keys())
217 #The list needs to be sorted so that we can
218 #avoid disabling child loggers of explicitly
219 #named loggers. With a sorted list it is easier
220 #to find the child loggers.
221 existing.sort()
222 #We'll keep the list of existing loggers
223 #which are children of named loggers here...
224 child_loggers = []
225 #now set up the new ones...
226 for log in llist:
227 section = cp["logger_%s" % log]
228 qn = section["qualname"]
229 propagate = section.getint("propagate", fallback=1)
230 logger = logging.getLogger(qn)
231 if qn in existing:
232 i = existing.index(qn) + 1 # start with the entry after qn
233 prefixed = qn + "."
234 pflen = len(prefixed)
235 num_existing = len(existing)
236 while i < num_existing:
237 if existing[i][:pflen] == prefixed:
238 child_loggers.append(existing[i])
239 i += 1
240 existing.remove(qn)
241 if "level" in section:
242 level = section["level"]
243 logger.setLevel(level)
244 for h in logger.handlers[:]:
245 logger.removeHandler(h)
246 logger.propagate = propagate
247 logger.disabled = 0
248 hlist = section["handlers"]
249 if len(hlist):
250 hlist = hlist.split(",")
251 hlist = _strip_spaces(hlist)
252 for hand in hlist:
253 logger.addHandler(handlers[hand])
254
255 #Disable any old loggers. There's no point deleting
256 #them as other threads may continue to hold references
257 #and by disabling them, you stop them doing any logging.
258 #However, don't disable children of named loggers, as that's
259 #probably not what was intended by the user.
260 #for log in existing:
261 # logger = root.manager.loggerDict[log]
262 # if log in child_loggers:
263 # logger.level = logging.NOTSET
264 # logger.handlers = []
265 # logger.propagate = 1
266 # elif disable_existing_loggers:
267 # logger.disabled = 1
268 _handle_existing_loggers(existing, child_loggers, disable_existing)
269
270
271def _clearExistingHandlers():
272 """Clear and close existing handlers"""
273 logging._handlers.clear()
274 logging.shutdown(logging._handlerList[:])
275 del logging._handlerList[:]
276
277
278IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
279
280
281def valid_ident(s):
282 m = IDENTIFIER.match(s)
283 if not m:
284 raise ValueError('Not a valid Python identifier: %r' % s)
285 return True
286
287
288class ConvertingMixin(object):
289 """For ConvertingXXX's, this mixin class provides common functions"""
290
291 def convert_with_key(self, key, value, replace=True):
292 result = self.configurator.convert(value)
293 #If the converted value is different, save for next time
294 if value is not result:
295 if replace:
296 self[key] = result
297 if type(result) in (ConvertingDict, ConvertingList,
298 ConvertingTuple):
299 result.parent = self
300 result.key = key
301 return result
302
303 def convert(self, value):
304 result = self.configurator.convert(value)
305 if value is not result:
306 if type(result) in (ConvertingDict, ConvertingList,
307 ConvertingTuple):
308 result.parent = self
309 return result
310
311
312# The ConvertingXXX classes are wrappers around standard Python containers,
313# and they serve to convert any suitable values in the container. The
314# conversion converts base dicts, lists and tuples to their wrapped
315# equivalents, whereas strings which match a conversion format are converted
316# appropriately.
317#
318# Each wrapper should have a configurator attribute holding the actual
319# configurator to use for conversion.
320
321class ConvertingDict(dict, ConvertingMixin):
322 """A converting dictionary wrapper."""
323
324 def __getitem__(self, key):
325 value = dict.__getitem__(self, key)
326 return self.convert_with_key(key, value)
327
328 def get(self, key, default=None):
329 value = dict.get(self, key, default)
330 return self.convert_with_key(key, value)
331
332 def pop(self, key, default=None):
333 value = dict.pop(self, key, default)
334 return self.convert_with_key(key, value, replace=False)
335
336class ConvertingList(list, ConvertingMixin):
337 """A converting list wrapper."""
338 def __getitem__(self, key):
339 value = list.__getitem__(self, key)
340 return self.convert_with_key(key, value)
341
342 def pop(self, idx=-1):
343 value = list.pop(self, idx)
344 return self.convert(value)
345
346class ConvertingTuple(tuple, ConvertingMixin):
347 """A converting tuple wrapper."""
348 def __getitem__(self, key):
349 value = tuple.__getitem__(self, key)
350 # Can't replace a tuple entry.
351 return self.convert_with_key(key, value, replace=False)
352
353class BaseConfigurator(object):
354 """
355 The configurator base class which defines some useful defaults.
356 """
357
358 CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
359
360 WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
361 DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
362 INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
363 DIGIT_PATTERN = re.compile(r'^\d+$')
364
365 value_converters = {
366 'ext' : 'ext_convert',
367 'cfg' : 'cfg_convert',
368 }
369
370 # We might want to use a different one, e.g. importlib
371 importer = staticmethod(__import__)
372
373 def __init__(self, config):
374 self.config = ConvertingDict(config)
375 self.config.configurator = self
376
377 def resolve(self, s):
378 """
379 Resolve strings to objects using standard import and attribute
380 syntax.
381 """
382 name = s.split('.')
383 used = name.pop(0)
384 try:
385 found = self.importer(used)
386 for frag in name:
387 used += '.' + frag
388 try:
389 found = getattr(found, frag)
390 except AttributeError:
391 self.importer(used)
392 found = getattr(found, frag)
393 return found
394 except ImportError:
395 e, tb = sys.exc_info()[1:]
396 v = ValueError('Cannot resolve %r: %s' % (s, e))
397 v.__cause__, v.__traceback__ = e, tb
398 raise v
399
400 def ext_convert(self, value):
401 """Default converter for the ext:// protocol."""
402 return self.resolve(value)
403
404 def cfg_convert(self, value):
405 """Default converter for the cfg:// protocol."""
406 rest = value
407 m = self.WORD_PATTERN.match(rest)
408 if m is None:
409 raise ValueError("Unable to convert %r" % value)
410 else:
411 rest = rest[m.end():]
412 d = self.config[m.groups()[0]]
413 #print d, rest
414 while rest:
415 m = self.DOT_PATTERN.match(rest)
416 if m:
417 d = d[m.groups()[0]]
418 else:
419 m = self.INDEX_PATTERN.match(rest)
420 if m:
421 idx = m.groups()[0]
422 if not self.DIGIT_PATTERN.match(idx):
423 d = d[idx]
424 else:
425 try:
426 n = int(idx) # try as number first (most likely)
427 d = d[n]
428 except TypeError:
429 d = d[idx]
430 if m:
431 rest = rest[m.end():]
432 else:
433 raise ValueError('Unable to convert '
434 '%r at %r' % (value, rest))
435 #rest should be empty
436 return d
437
438 def convert(self, value):
439 """
440 Convert values to an appropriate type. dicts, lists and tuples are
441 replaced by their converting alternatives. Strings are checked to
442 see if they have a conversion format and are converted if they do.
443 """
444 if not isinstance(value, ConvertingDict) and isinstance(value, dict):
445 value = ConvertingDict(value)
446 value.configurator = self
447 elif not isinstance(value, ConvertingList) and isinstance(value, list):
448 value = ConvertingList(value)
449 value.configurator = self
450 elif not isinstance(value, ConvertingTuple) and\
451 isinstance(value, tuple) and not hasattr(value, '_fields'):
452 value = ConvertingTuple(value)
453 value.configurator = self
454 elif isinstance(value, str): # str for py3k
455 m = self.CONVERT_PATTERN.match(value)
456 if m:
457 d = m.groupdict()
458 prefix = d['prefix']
459 converter = self.value_converters.get(prefix, None)
460 if converter:
461 suffix = d['suffix']
462 converter = getattr(self, converter)
463 value = converter(suffix)
464 return value
465
466 def configure_custom(self, config):
467 """Configure an object with a user-supplied factory."""
468 c = config.pop('()')
469 if not callable(c):
470 c = self.resolve(c)
471 props = config.pop('.', None)
472 # Check for valid identifiers
473 kwargs = {k: config[k] for k in config if valid_ident(k)}
474 result = c(**kwargs)
475 if props:
476 for name, value in props.items():
477 setattr(result, name, value)
478 return result
479
480 def as_tuple(self, value):
481 """Utility function which converts lists to tuples."""
482 if isinstance(value, list):
483 value = tuple(value)
484 return value
485
486class DictConfigurator(BaseConfigurator):
487 """
488 Configure logging using a dictionary-like object to describe the
489 configuration.
490 """
491
492 def configure(self):
493 """Do the configuration."""
494
495 config = self.config
496 if 'version' not in config:
497 raise ValueError("dictionary doesn't specify a version")
498 if config['version'] != 1:
499 raise ValueError("Unsupported version: %s" % config['version'])
500 incremental = config.pop('incremental', False)
501 EMPTY_DICT = {}
502 logging._acquireLock()
503 try:
504 if incremental:
505 handlers = config.get('handlers', EMPTY_DICT)
506 for name in handlers:
507 if name not in logging._handlers:
508 raise ValueError('No handler found with '
509 'name %r' % name)
510 else:
511 try:
512 handler = logging._handlers[name]
513 handler_config = handlers[name]
514 level = handler_config.get('level', None)
515 if level:
516 handler.setLevel(logging._checkLevel(level))
517 except Exception as e:
518 raise ValueError('Unable to configure handler '
519 '%r' % name) from e
520 loggers = config.get('loggers', EMPTY_DICT)
521 for name in loggers:
522 try:
523 self.configure_logger(name, loggers[name], True)
524 except Exception as e:
525 raise ValueError('Unable to configure logger '
526 '%r' % name) from e
527 root = config.get('root', None)
528 if root:
529 try:
530 self.configure_root(root, True)
531 except Exception as e:
532 raise ValueError('Unable to configure root '
533 'logger') from e
534 else:
535 disable_existing = config.pop('disable_existing_loggers', True)
536
537 _clearExistingHandlers()
538
539 # Do formatters first - they don't refer to anything else
540 formatters = config.get('formatters', EMPTY_DICT)
541 for name in formatters:
542 try:
543 formatters[name] = self.configure_formatter(
544 formatters[name])
545 except Exception as e:
546 raise ValueError('Unable to configure '
547 'formatter %r' % name) from e
548 # Next, do filters - they don't refer to anything else, either
549 filters = config.get('filters', EMPTY_DICT)
550 for name in filters:
551 try:
552 filters[name] = self.configure_filter(filters[name])
553 except Exception as e:
554 raise ValueError('Unable to configure '
555 'filter %r' % name) from e
556
557 # Next, do handlers - they refer to formatters and filters
558 # As handlers can refer to other handlers, sort the keys
559 # to allow a deterministic order of configuration
560 handlers = config.get('handlers', EMPTY_DICT)
561 deferred = []
562 for name in sorted(handlers):
563 try:
564 handler = self.configure_handler(handlers[name])
565 handler.name = name
566 handlers[name] = handler
567 except Exception as e:
568 if 'target not configured yet' in str(e.__cause__):
569 deferred.append(name)
570 else:
571 raise ValueError('Unable to configure handler '
572 '%r' % name) from e
573
574 # Now do any that were deferred
575 for name in deferred:
576 try:
577 handler = self.configure_handler(handlers[name])
578 handler.name = name
579 handlers[name] = handler
580 except Exception as e:
581 raise ValueError('Unable to configure handler '
582 '%r' % name) from e
583
584 # Next, do loggers - they refer to handlers and filters
585
586 #we don't want to lose the existing loggers,
587 #since other threads may have pointers to them.
588 #existing is set to contain all existing loggers,
589 #and as we go through the new configuration we
590 #remove any which are configured. At the end,
591 #what's left in existing is the set of loggers
592 #which were in the previous configuration but
593 #which are not in the new configuration.
594 root = logging.root
595 existing = list(root.manager.loggerDict.keys())
596 #The list needs to be sorted so that we can
597 #avoid disabling child loggers of explicitly
598 #named loggers. With a sorted list it is easier
599 #to find the child loggers.
600 existing.sort()
601 #We'll keep the list of existing loggers
602 #which are children of named loggers here...
603 child_loggers = []
604 #now set up the new ones...
605 loggers = config.get('loggers', EMPTY_DICT)
606 for name in loggers:
607 if name in existing:
608 i = existing.index(name) + 1 # look after name
609 prefixed = name + "."
610 pflen = len(prefixed)
611 num_existing = len(existing)
612 while i < num_existing:
613 if existing[i][:pflen] == prefixed:
614 child_loggers.append(existing[i])
615 i += 1
616 existing.remove(name)
617 try:
618 self.configure_logger(name, loggers[name])
619 except Exception as e:
620 raise ValueError('Unable to configure logger '
621 '%r' % name) from e
622
623 #Disable any old loggers. There's no point deleting
624 #them as other threads may continue to hold references
625 #and by disabling them, you stop them doing any logging.
626 #However, don't disable children of named loggers, as that's
627 #probably not what was intended by the user.
628 #for log in existing:
629 # logger = root.manager.loggerDict[log]
630 # if log in child_loggers:
631 # logger.level = logging.NOTSET
632 # logger.handlers = []
633 # logger.propagate = True
634 # elif disable_existing:
635 # logger.disabled = True
636 _handle_existing_loggers(existing, child_loggers,
637 disable_existing)
638
639 # And finally, do the root logger
640 root = config.get('root', None)
641 if root:
642 try:
643 self.configure_root(root)
644 except Exception as e:
645 raise ValueError('Unable to configure root '
646 'logger') from e
647 finally:
648 logging._releaseLock()
649
650 def configure_formatter(self, config):
651 """Configure a formatter from a dictionary."""
652 if '()' in config:
653 factory = config['()'] # for use in exception handler
654 try:
655 result = self.configure_custom(config)
656 except TypeError as te:
657 if "'format'" not in str(te):
658 raise
659 #Name of parameter changed from fmt to format.
660 #Retry with old name.
661 #This is so that code can be used with older Python versions
662 #(e.g. by Django)
663 config['fmt'] = config.pop('format')
664 config['()'] = factory
665 result = self.configure_custom(config)
666 else:
667 fmt = config.get('format', None)
668 dfmt = config.get('datefmt', None)
669 style = config.get('style', '%')
670 cname = config.get('class', None)
671
672 if not cname:
673 c = logging.Formatter
674 else:
675 c = _resolve(cname)
676
677 # A TypeError would be raised if "validate" key is passed in with a formatter callable
678 # that does not accept "validate" as a parameter
679 if 'validate' in config: # if user hasn't mentioned it, the default will be fine
680 result = c(fmt, dfmt, style, config['validate'])
681 else:
682 result = c(fmt, dfmt, style)
683
684 return result
685
686 def configure_filter(self, config):
687 """Configure a filter from a dictionary."""
688 if '()' in config:
689 result = self.configure_custom(config)
690 else:
691 name = config.get('name', '')
692 result = logging.Filter(name)
693 return result
694
695 def add_filters(self, filterer, filters):
696 """Add filters to a filterer from a list of names."""
697 for f in filters:
698 try:
699 filterer.addFilter(self.config['filters'][f])
700 except Exception as e:
701 raise ValueError('Unable to add filter %r' % f) from e
702
703 def configure_handler(self, config):
704 """Configure a handler from a dictionary."""
705 config_copy = dict(config) # for restoring in case of error
706 formatter = config.pop('formatter', None)
707 if formatter:
708 try:
709 formatter = self.config['formatters'][formatter]
710 except Exception as e:
711 raise ValueError('Unable to set formatter '
712 '%r' % formatter) from e
713 level = config.pop('level', None)
714 filters = config.pop('filters', None)
715 if '()' in config:
716 c = config.pop('()')
717 if not callable(c):
718 c = self.resolve(c)
719 factory = c
720 else:
721 cname = config.pop('class')
722 klass = self.resolve(cname)
723 #Special case for handler which refers to another handler
724 if issubclass(klass, logging.handlers.MemoryHandler) and\
725 'target' in config:
726 try:
727 th = self.config['handlers'][config['target']]
728 if not isinstance(th, logging.Handler):
729 config.update(config_copy) # restore for deferred cfg
730 raise TypeError('target not configured yet')
731 config['target'] = th
732 except Exception as e:
733 raise ValueError('Unable to set target handler '
734 '%r' % config['target']) from e
735 elif issubclass(klass, logging.handlers.SMTPHandler) and\
736 'mailhost' in config:
737 config['mailhost'] = self.as_tuple(config['mailhost'])
738 elif issubclass(klass, logging.handlers.SysLogHandler) and\
739 'address' in config:
740 config['address'] = self.as_tuple(config['address'])
741 factory = klass
742 props = config.pop('.', None)
743 kwargs = {k: config[k] for k in config if valid_ident(k)}
744 try:
745 result = factory(**kwargs)
746 except TypeError as te:
747 if "'stream'" not in str(te):
748 raise
749 #The argument name changed from strm to stream
750 #Retry with old name.
751 #This is so that code can be used with older Python versions
752 #(e.g. by Django)
753 kwargs['strm'] = kwargs.pop('stream')
754 result = factory(**kwargs)
755 if formatter:
756 result.setFormatter(formatter)
757 if level is not None:
758 result.setLevel(logging._checkLevel(level))
759 if filters:
760 self.add_filters(result, filters)
761 if props:
762 for name, value in props.items():
763 setattr(result, name, value)
764 return result
765
766 def add_handlers(self, logger, handlers):
767 """Add handlers to a logger from a list of names."""
768 for h in handlers:
769 try:
770 logger.addHandler(self.config['handlers'][h])
771 except Exception as e:
772 raise ValueError('Unable to add handler %r' % h) from e
773
774 def common_logger_config(self, logger, config, incremental=False):
775 """
776 Perform configuration which is common to root and non-root loggers.
777 """
778 level = config.get('level', None)
779 if level is not None:
780 logger.setLevel(logging._checkLevel(level))
781 if not incremental:
782 #Remove any existing handlers
783 for h in logger.handlers[:]:
784 logger.removeHandler(h)
785 handlers = config.get('handlers', None)
786 if handlers:
787 self.add_handlers(logger, handlers)
788 filters = config.get('filters', None)
789 if filters:
790 self.add_filters(logger, filters)
791
792 def configure_logger(self, name, config, incremental=False):
793 """Configure a non-root logger from a dictionary."""
794 logger = logging.getLogger(name)
795 self.common_logger_config(logger, config, incremental)
796 propagate = config.get('propagate', None)
797 if propagate is not None:
798 logger.propagate = propagate
799
800 def configure_root(self, config, incremental=False):
801 """Configure a root logger from a dictionary."""
802 root = logging.getLogger()
803 self.common_logger_config(root, config, incremental)
804
805dictConfigClass = DictConfigurator
806
807def dictConfig(config):
808 """Configure logging using a dictionary."""
809 dictConfigClass(config).configure()
810
811
812def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None):
813 """
814 Start up a socket server on the specified port, and listen for new
815 configurations.
816
817 These will be sent as a file suitable for processing by fileConfig().
818 Returns a Thread object on which you can call start() to start the server,
819 and which you can join() when appropriate. To stop the server, call
820 stopListening().
821
822 Use the ``verify`` argument to verify any bytes received across the wire
823 from a client. If specified, it should be a callable which receives a
824 single argument - the bytes of configuration data received across the
825 network - and it should return either ``None``, to indicate that the
826 passed in bytes could not be verified and should be discarded, or a
827 byte string which is then passed to the configuration machinery as
828 normal. Note that you can return transformed bytes, e.g. by decrypting
829 the bytes passed in.
830 """
831
832 class ConfigStreamHandler(StreamRequestHandler):
833 """
834 Handler for a logging configuration request.
835
836 It expects a completely new logging configuration and uses fileConfig
837 to install it.
838 """
839 def handle(self):
840 """
841 Handle a request.
842
843 Each request is expected to be a 4-byte length, packed using
844 struct.pack(">L", n), followed by the config file.
845 Uses fileConfig() to do the grunt work.
846 """
847 try:
848 conn = self.connection
849 chunk = conn.recv(4)
850 if len(chunk) == 4:
851 slen = struct.unpack(">L", chunk)[0]
852 chunk = self.connection.recv(slen)
853 while len(chunk) < slen:
854 chunk = chunk + conn.recv(slen - len(chunk))
855 if self.server.verify is not None:
856 chunk = self.server.verify(chunk)
857 if chunk is not None: # verified, can process
858 chunk = chunk.decode("utf-8")
859 try:
860 import json
861 d =json.loads(chunk)
862 assert isinstance(d, dict)
863 dictConfig(d)
864 except Exception:
865 #Apply new configuration.
866
867 file = io.StringIO(chunk)
868 try:
869 fileConfig(file)
870 except Exception:
871 traceback.print_exc()
872 if self.server.ready:
873 self.server.ready.set()
874 except OSError as e:
875 if e.errno != RESET_ERROR:
876 raise
877
878 class ConfigSocketReceiver(ThreadingTCPServer):
879 """
880 A simple TCP socket-based logging config receiver.
881 """
882
883 allow_reuse_address = 1
884
885 def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
886 handler=None, ready=None, verify=None):
887 ThreadingTCPServer.__init__(self, (host, port), handler)
888 logging._acquireLock()
889 self.abort = 0
890 logging._releaseLock()
891 self.timeout = 1
892 self.ready = ready
893 self.verify = verify
894
895 def serve_until_stopped(self):
896 import select
897 abort = 0
898 while not abort:
899 rd, wr, ex = select.select([self.socket.fileno()],
900 [], [],
901 self.timeout)
902 if rd:
903 self.handle_request()
904 logging._acquireLock()
905 abort = self.abort
906 logging._releaseLock()
907 self.server_close()
908
909 class Server(threading.Thread):
910
911 def __init__(self, rcvr, hdlr, port, verify):
912 super(Server, self).__init__()
913 self.rcvr = rcvr
914 self.hdlr = hdlr
915 self.port = port
916 self.verify = verify
917 self.ready = threading.Event()
918
919 def run(self):
920 server = self.rcvr(port=self.port, handler=self.hdlr,
921 ready=self.ready,
922 verify=self.verify)
923 if self.port == 0:
924 self.port = server.server_address[1]
925 self.ready.set()
926 global _listener
927 logging._acquireLock()
928 _listener = server
929 logging._releaseLock()
930 server.serve_until_stopped()
931
932 return Server(ConfigSocketReceiver, ConfigStreamHandler, port, verify)
933
934def stopListening():
935 """
936 Stop the listening server which was created with a call to listen().
937 """
938 global _listener
939 logging._acquireLock()
940 try:
941 if _listener:
942 _listener.abort = 1
943 _listener = None
944 finally:
945 logging._releaseLock()