# Copyright (c) 2018 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. import logging import threading import os import sys import functools __all__ = ['patch_loggingMDC', 'MDC'] _replace_func_name = ['info', 'critical', 'fatal', 'debug', 'error', 'warn', 'warning', 'findCaller'] class MDCContext(threading.local): """ A Thread local instance to storage mdc values """ def __init__(self): super(MDCContext, self).__init__() self._localDict = {} def get(self, key): return self._localDict.get(key, None) def put(self, key, value): self._localDict[key] = value def remove(self, key): if key in self.localDict: del self._localDict[key] def clear(self): self._localDict.clear() def result(self): return self._localDict def isEmpty(self): return self._localDict == {} or self._localDict is None MDC = MDCContext() def fetchkeys(func): @functools.wraps(func) def replace(*args, **kwargs): kwargs['extra'] = _getmdcs(extra=kwargs.get('extra', None)) func(*args, **kwargs) return replace def _getmdcs(extra=None): """ Put mdc dict in logging record extra filed with key 'mdc' :param extra: dict :return: mdc dict """ if MDC.isEmpty(): return mdc = MDC.result() if extra is not None: for key in extra: # make sure extra key dosen't override mdckey if key in mdc or key == 'mdc': raise KeyError("Attempt to overwrite %r in MDC" % key) else: extra = {} extra['mdc'] = mdc del mdc return extra @fetchkeys def info(self, msg, *args, **kwargs): if self.isEnabledFor(logging.INFO): self._log(logging.INFO, msg, args, **kwargs) @fetchkeys def debug(self, msg, *args, **kwargs): if self.isEnabledFor(logging.DEBUG): self._log(logging.DEBUG, msg, args, **kwargs) @fetchkeys def warning(self, msg, *args, **kwargs): if self.isEnabledFor(logging.WARNING): self._log(logging.WARNING, msg, args, **kwargs) @fetchkeys def exception(self, msg, *args, **kwargs): kwargs['exc_info'] = 1 self.error(msg, *args, **kwargs) @fetchkeys def critical(self, msg, *args, **kwargs): if self.isEnabledFor(logging.CRITICAL): self._log(logging.CRITICAL, msg, args, **kwargs) @fetchkeys def error(self, msg, *args, **kwargs): if self.isEnabledFor(logging.ERROR): self._log(logging.ERROR, msg, args, **kwargs) def findCaller(self): f = logging.currentframe() if f is not None: f = f.f_back rv = "(unkown file)", 0, "(unknow function)" while hasattr(f, "f_code"): co = f.f_code filename = os.path.normcase(co.co_filename) # jump through local 'replace' func frame if filename == logging._srcfile or co.co_name == "replace": f = f.f_back continue rv = (co.co_filename, f.f_lineno, co.co_name) break return rv def patch_loggingMDC(): """ The patch to add MDC ability in logging Record instance at runtime """ localModule = sys.modules[__name__] for attr in dir(logging.Logger): if attr in _replace_func_name: newfunc = getattr(localModule, attr, None) if newfunc: setattr(logging.Logger, attr, newfunc)