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