onaplogging: Docstrings, refactor, type hinting
[logging-analytics.git] / pylog / onaplogging / mdcContext.py
1 # Copyright 2018 ke liang <lokyse@163.com>.
2 #
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
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 import logging
16 import threading
17 import io
18 import os
19 import traceback
20 import sys
21 import functools
22
23 from deprecated import deprecated
24 from typing import Dict, Optional, Any, Callable, List, Tuple
25 from logging import LogRecord
26
27 from onaplogging.utils.system import is_above_python_3_2
28
29 from .marker import Marker, MARKER_TAG
30
31 # TODO change to patch_logging_mdc after deprecated method is removed
32 __all__ = ['patch_loggingMDC', 'MDC']
33
34 _replace_func_name = ['info', 'critical', 'fatal', 'debug',
35                       'error', 'warn', 'warning', 'log',
36                       'handle', 'findCaller']
37
38
39 def fetchkeys(func):  # type: Callable[[str, List, Dict], None]
40     # type: (...) -> Callable[[str, List, Dict], None]
41     """MDC decorator.
42
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.
46     """
47
48     @functools.wraps(func)
49     def replace(*args, **kwargs):
50         # type: () -> None
51         kwargs['extra'] = _getmdcs(extra=kwargs.get('extra', None))
52         func(*args, **kwargs)
53
54     return replace
55
56
57 class MDCContext(threading.local):
58     """A Thread local instance that stores MDC values.
59
60     Is initializ with an empty dictionary. Manages that
61     dictionary to created Mapped Diagnostic Context.
62
63     Extends:
64         threading.local
65     Property:
66         local_dict  : a placeholder for MDC keys and values.
67     """
68
69     @property
70     def local_dict(self):
71         # type: () -> Dict
72         return self._local_dict
73
74     @local_dict.setter
75     def local_dict(self, value):
76         # type: (Dict) -> None
77         self._local_dict = value
78
79     def __init__(self):
80         super(MDCContext, self).__init__()
81         self.local_dict = {}
82
83     def get(self, key):
84         # type: (str) -> Any
85         """Retrieve a value by key."""
86         return self.local_dict.get(key, None)
87
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
92
93     def remove(self, key):
94         # type: (str) -> None
95         """Remove a value by key, if exists."""
96         if key in self.local_dict:
97             del self.local_dict[key]
98
99     def clear(self):
100         # type: () -> None
101         """Empty the MDC dictionary."""
102         self.local_dict.clear()
103
104     @deprecated(reason="Use local_mdc property instead.")
105     def result(self):
106         """Getter for the MDC dictionary."""
107         return self.local_dict
108
109     def empty(self):
110         # type: () -> bool
111         """Checks whether the local dictionary is empty."""
112         return self.local_dict == {} or \
113                self.local_dict is None
114
115     @deprecated(reason="Will be replaced. Use empty() instead.")
116     def isEmpty(self):
117         """See empty()."""
118         return self.empty()
119
120
121 MDC = MDCContext()
122
123
124 def _getmdcs(extra=None):
125     # type: (Optional[Dict]) -> Dict
126     """
127     Puts an MDC dict in the `extra` field with key 'mdc'. This provides
128     the contextual information with MDC.
129
130     Args:
131         extra       : Contextual information.       Defaults to None.
132     Raises:
133         KeyError    : a key from extra is attempted to be overwritten.
134     Returns:
135         dict        : contextual information named `extra` with MDC.
136     """
137     if MDC.empty():
138         return extra
139
140     mdc = MDC.local_dict
141
142     if extra is not None:
143         for key in extra:
144             if  key in mdc or \
145                 key == 'mdc':
146                 raise KeyError("Attempt to overwrite %r in MDC" % key)
147     else:
148         extra = {}
149
150     extra['mdc'] = mdc
151     del mdc
152
153     return extra
154
155
156 @fetchkeys
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)
162
163
164 @fetchkeys
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)
170
171
172 @fetchkeys
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)
178
179
180 @fetchkeys
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)
186
187
188 @fetchkeys
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)
194
195
196 @fetchkeys
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)
202
203
204 @fetchkeys
205 def log(self, level, msg, *args, **kwargs):
206     # type: (int, str) -> None
207     """
208     If a specific logging level enabled and the code is represented
209     as an integer value, delegate the call to the underlying logger.
210
211     Raises:
212         TypeError: if the logging level code is not an integer.
213     """
214
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))
219         else:
220             return
221
222     if self.isEnabledFor(level):
223         self._log(level, msg, args, **kwargs)
224
225
226 def handle(self, record):
227     # type: (LogRecord) -> None
228     cmarker = getattr(self, MARKER_TAG, None)
229
230     if isinstance(cmarker, Marker):
231         setattr(record, MARKER_TAG, cmarker)
232
233     if not self.disabled and \
234        self.filter(record):
235         self.callHandlers(record)
236
237
238 def findCaller(self, stack_info=False):
239     # type: (bool) -> Tuple
240     """
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().
243     """
244
245     frame = logging.currentframe()
246
247     if frame is not None:
248         frame = frame.f_back
249     rv = "(unkown file)", 0, "(unknow function)"
250
251     while hasattr(frame, "f_code"):
252         co = 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":
257
258             frame = frame.f_back
259             continue
260
261         if is_above_python_3_2():
262
263             sinfo = None
264             if stack_info:
265
266                 sio = io.StringIO()
267                 sio.write("Stack (most recent call last):\n")
268                 traceback.print_stack(frame, file=sio)
269                 sinfo = sio.getvalue()
270
271                 if sinfo[-1] == '\n':
272                     sinfo = sinfo[:-1]
273
274                 sio.close()
275             rv = (co.co_filename, frame.f_lineno, co.co_name, sinfo)
276
277         else:
278             rv = (co.co_filename, frame.f_lineno, co.co_name)
279
280         break
281
282     return rv
283
284
285 def patch_logging_mdc():
286     # type: () -> None
287     """MDC patch.
288
289     Sets MDC in a logging record instance at runtime.
290     """
291     localModule = sys.modules[__name__]
292
293     for attr in dir(logging.Logger):
294         if attr in _replace_func_name:
295             newfunc = getattr(localModule, attr, None)
296             if newfunc:
297                 setattr(logging.Logger, attr, newfunc)
298
299
300 @deprecated(reason="Will be removed. Call patch_logging_mdc() instead.")
301 def patch_loggingMDC():
302     """See patch_logging_ymdc()."""
303     patch_logging_mdc()