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