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 os 32import queue 33import re 34import struct 35import threading 36import traceback 37 38from socketserver import ThreadingTCPServer, StreamRequestHandler 39 40 41DEFAULT_LOGGING_CONFIG_PORT = 9030 42 43RESET_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 52def 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 96def _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 110def _strip_spaces(alist): 111 return map(str.strip, alist) 112 113def _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 135def _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 173def _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 195def _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 282def _clearExistingHandlers(): 283 """Clear and close existing handlers""" 284 logging._handlers.clear() 285 logging.shutdown(logging._handlerList[:]) 286 del logging._handlerList[:] 287 288 289IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) 290 291 292def 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 299class ConvertingMixin(object): 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 332class ConvertingDict(dict, ConvertingMixin): 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 347class ConvertingList(list, ConvertingMixin): 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 357class ConvertingTuple(tuple, ConvertingMixin): 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 364class BaseConfigurator(object): 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 props = config.pop('.', None) 481 # Check for valid identifiers 482 kwargs = {k: config[k] for k in config if valid_ident(k)} 483 result = c(**kwargs) 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 495class DictConfigurator(BaseConfigurator): 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 props = config.pop('.', None) 756 kwargs = {k: config[k] for k in config if valid_ident(k)} 757 try: 758 result = factory(**kwargs) 759 except TypeError as te: 760 if "'stream'" not in str(te): 761 raise 762 #The argument name changed from strm to stream 763 #Retry with old name. 764 #This is so that code can be used with older Python versions 765 #(e.g. by Django) 766 kwargs['strm'] = kwargs.pop('stream') 767 result = factory(**kwargs) 768 if formatter: 769 result.setFormatter(formatter) 770 if level is not None: 771 result.setLevel(logging._checkLevel(level)) 772 if filters: 773 self.add_filters(result, filters) 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 819dictConfigClass = DictConfigurator 820 821def dictConfig(config): 822 """Configure logging using a dictionary.""" 823 dictConfigClass(config).configure() 824 825 826def 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 ConfigStreamHandler(StreamRequestHandler): 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 ConfigSocketReceiver(ThreadingTCPServer): 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 Server(threading.Thread): 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 948def 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() 960