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