1 # Copyright 2018 ke liang <lokyse@163.com>.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
23 from deprecated import deprecated
24 from typing import Dict, Optional, Any, Callable, List, Tuple
25 from logging import LogRecord
27 from onaplogging.utils.system import is_above_python_3_2
29 from .marker import Marker, MARKER_TAG
31 # TODO change to patch_logging_mdc after deprecated method is removed
32 __all__ = ['patch_loggingMDC', 'MDC']
34 _replace_func_name = ['info', 'critical', 'fatal', 'debug',
35 'error', 'warn', 'warning', 'log',
36 'handle', 'findCaller']
39 def fetchkeys(func): # type: Callable[[str, List, Dict], None]
40 # type: (...) -> Callable[[str, List, Dict], None]
43 Fetchs contextual information from a logging call.
44 Wraps by adding MDC to the `extra` field. Executes
45 the call with the updated contextual information.
48 @functools.wraps(func)
49 def replace(*args, **kwargs):
51 kwargs['extra'] = _getmdcs(extra=kwargs.get('extra', None))
57 class MDCContext(threading.local):
58 """A Thread local instance that stores MDC values.
60 Is initializ with an empty dictionary. Manages that
61 dictionary to created Mapped Diagnostic Context.
66 local_dict : a placeholder for MDC keys and values.
72 return self._local_dict
75 def local_dict(self, value):
76 # type: (Dict) -> None
77 self._local_dict = value
80 super(MDCContext, self).__init__()
85 """Retrieve a value by key."""
86 return self.local_dict.get(key, None)
88 def put(self, key, value):
89 # type: (str, Any) -> None
90 """Insert or update a value by key."""
91 self.local_dict[key] = value
93 def remove(self, key):
95 """Remove a value by key, if exists."""
96 if key in self.local_dict:
97 del self.local_dict[key]
101 """Empty the MDC dictionary."""
102 self.local_dict.clear()
104 @deprecated(reason="Use local_mdc property instead.")
106 """Getter for the MDC dictionary."""
107 return self.local_dict
111 """Checks whether the local dictionary is empty."""
112 return self.local_dict == {} or \
113 self.local_dict is None
115 @deprecated(reason="Will be replaced. Use empty() instead.")
124 def _getmdcs(extra=None):
125 # type: (Optional[Dict]) -> Dict
127 Puts an MDC dict in the `extra` field with key 'mdc'. This provides
128 the contextual information with MDC.
131 extra : Contextual information. Defaults to None.
133 KeyError : a key from extra is attempted to be overwritten.
135 dict : contextual information named `extra` with MDC.
142 if extra is not None:
146 raise KeyError("Attempt to overwrite %r in MDC" % key)
157 def info(self, msg, *args, **kwargs):
158 # type: (str) -> None
159 """If INFO enabled, deletage an info call with MDC."""
160 if self.isEnabledFor(logging.INFO):
161 self._log(logging.INFO, msg, args, **kwargs)
165 def debug(self, msg, *args, **kwargs):
166 # type: (str) -> None
167 """If DEBUG enabled, deletage a debug call with MDC."""
168 if self.isEnabledFor(logging.DEBUG):
169 self._log(logging.DEBUG, msg, args, **kwargs)
173 def warning(self, msg, *args, **kwargs):
174 # type: (str) -> None
175 """If WARNING enabled, deletage a warning call with MDC."""
176 if self.isEnabledFor(logging.WARNING):
177 self._log(logging.WARNING, msg, args, **kwargs)
181 def exception(self, msg, *args, **kwargs):
182 # type: (str) -> None
183 """Deletage an exception call and set exc_info code to 1."""
184 kwargs['exc_info'] = 1
185 self.error(msg, *args, **kwargs)
189 def critical(self, msg, *args, **kwargs):
190 # type: (str) -> None
191 """If CRITICAL enabled, deletage a critical call with MDC."""
192 if self.isEnabledFor(logging.CRITICAL):
193 self._log(logging.CRITICAL, msg, args, **kwargs)
197 def error(self, msg, *args, **kwargs):
198 # type: (str) -> None
199 """If ERROR enabled, deletage an error call with MDC."""
200 if self.isEnabledFor(logging.ERROR):
201 self._log(logging.ERROR, msg, args, **kwargs)
205 def log(self, level, msg, *args, **kwargs):
206 # type: (int, str) -> None
208 If a specific logging level enabled and the code is represented
209 as an integer value, delegate the call to the underlying logger.
212 TypeError: if the logging level code is not an integer.
215 if not isinstance(level, int):
216 if logging.raiseExceptions:
217 raise TypeError("Logging level code must be an integer."
218 "Got %s instead." % type(level))
222 if self.isEnabledFor(level):
223 self._log(level, msg, args, **kwargs)
226 def handle(self, record):
227 # type: (LogRecord) -> None
228 cmarker = getattr(self, MARKER_TAG, None)
230 if isinstance(cmarker, Marker):
231 setattr(record, MARKER_TAG, cmarker)
233 if not self.disabled and \
235 self.callHandlers(record)
238 def findCaller(self, stack_info=False):
239 # type: (bool) -> Tuple
241 Find the stack frame of the caller so that we can note the source file
242 name, line number and function name. Enhances the logging.findCaller().
245 frame = logging.currentframe()
247 if frame is not None:
249 rv = "(unkown file)", 0, "(unknow function)"
251 while hasattr(frame, "f_code"):
253 filename = os.path.normcase(co.co_filename)
254 # jump through local 'replace' func frame
255 if filename == logging._srcfile or \
256 co.co_name == "replace":
261 if is_above_python_3_2():
267 sio.write("Stack (most recent call last):\n")
268 traceback.print_stack(frame, file=sio)
269 sinfo = sio.getvalue()
271 if sinfo[-1] == '\n':
275 rv = (co.co_filename, frame.f_lineno, co.co_name, sinfo)
278 rv = (co.co_filename, frame.f_lineno, co.co_name)
285 def patch_logging_mdc():
289 Sets MDC in a logging record instance at runtime.
291 localModule = sys.modules[__name__]
293 for attr in dir(logging.Logger):
294 if attr in _replace_func_name:
295 newfunc = getattr(localModule, attr, None)
297 setattr(logging.Logger, attr, newfunc)
300 @deprecated(reason="Will be removed. Call patch_logging_mdc() instead.")
301 def patch_loggingMDC():
302 """See patch_logging_ymdc()."""