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