Coverage for python/lsst/log/log/logContinued.py : 42%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3#
4# LSST Data Management System
5# Copyright 2013 LSST Corporation.
6#
7# This product includes software developed by the
8# LSST Project (http://www.lsst.org/).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the LSST License Statement and
21# the GNU General Public License along with this program. If not,
22# see <http://www.lsstcorp.org/LegalNotices/>.
23#
25__all__ = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "CRITICAL", "WARNING",
26 "Log", "configure", "configure_prop", "configure_pylog_MDC", "getDefaultLogger",
27 "getLogger", "MDC", "MDCDict", "MDCRemove", "MDCRegisterInit", "setLevel",
28 "getLevel", "isEnabledFor", "log", "trace", "debug", "info", "warn", "warning",
29 "error", "fatal", "critical", "logf", "tracef", "debugf", "infof", "warnf", "errorf", "fatalf",
30 "lwpID", "usePythonLogging", "doNotUsePythonLogging", "UsePythonLogging",
31 "LevelTranslator", "LogHandler", "getEffectiveLevel", "getLevelName"]
33import logging
34import inspect
35import os
37from typing import Optional
38from deprecated.sphinx import deprecated
40from lsst.utils import continueClass
42from .log import Log
44TRACE = 5000
45DEBUG = 10000
46INFO = 20000
47WARN = 30000
48ERROR = 40000
49FATAL = 50000
51# For compatibility with python logging
52CRITICAL = FATAL
53WARNING = WARN
56@continueClass # noqa: F811 (FIXME: remove for py 3.8+)
57class Log: # noqa: F811
58 UsePythonLogging = False
59 """Forward Python `lsst.log` messages to Python `logging` package."""
61 CRITICAL = CRITICAL
62 WARNING = WARNING
64 @classmethod
65 def usePythonLogging(cls):
66 """Forward log messages to Python `logging`
68 Notes
69 -----
70 This is useful for unit testing when you want to ensure
71 that log messages are captured by the testing environment
72 as distinct from standard output.
74 This state only affects messages sent to the `lsst.log`
75 package from Python.
76 """
77 cls.UsePythonLogging = True
79 @classmethod
80 def doNotUsePythonLogging(cls):
81 """Forward log messages to LSST logging system.
83 Notes
84 -----
85 This is the default state.
86 """
87 cls.UsePythonLogging = False
89 @property
90 def name(self):
91 return self.getName()
93 @property
94 def level(self):
95 return self.getLevel()
97 @property
98 def parent(self):
99 """Returns the parent logger, or None if this is the root logger."""
100 if not self.name:
101 return None
102 parent_name = self.name.rpartition(".")[0]
103 if not parent_name:
104 return self.getDefaultLogger()
105 return self.getLogger(parent_name)
107 def trace(self, fmt, *args):
108 self._log(Log.TRACE, False, fmt, *args)
110 def debug(self, fmt, *args):
111 self._log(Log.DEBUG, False, fmt, *args)
113 def info(self, fmt, *args):
114 self._log(Log.INFO, False, fmt, *args)
116 def warn(self, fmt, *args):
117 self._log(Log.WARN, False, fmt, *args)
119 def warning(self, fmt, *args):
120 self.warn(fmt, *args)
122 def error(self, fmt, *args):
123 self._log(Log.ERROR, False, fmt, *args)
125 def fatal(self, fmt, *args):
126 self._log(Log.FATAL, False, fmt, *args)
128 def critical(self, fmt, *args):
129 self.fatal(fmt, *args)
131 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
132 " Will be removed after v25",
133 version="v23.0", category=FutureWarning)
134 def tracef(self, fmt, *args, **kwargs):
135 self._log(Log.TRACE, True, fmt, *args, **kwargs)
137 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
138 " Will be removed after v25",
139 version="v23.0", category=FutureWarning)
140 def debugf(self, fmt, *args, **kwargs):
141 self._log(Log.DEBUG, True, fmt, *args, **kwargs)
143 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
144 " Will be removed after v25",
145 version="v23.0", category=FutureWarning)
146 def infof(self, fmt, *args, **kwargs):
147 self._log(Log.INFO, True, fmt, *args, **kwargs)
149 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
150 " Will be removed after v25",
151 version="v23.0", category=FutureWarning)
152 def warnf(self, fmt, *args, **kwargs):
153 self._log(Log.WARN, True, fmt, *args, **kwargs)
155 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
156 " Will be removed after v25",
157 version="v23.0", category=FutureWarning)
158 def errorf(self, fmt, *args, **kwargs):
159 self._log(Log.ERROR, True, fmt, *args, **kwargs)
161 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
162 " Will be removed after v25",
163 version="v23.0", category=FutureWarning)
164 def fatalf(self, fmt, *args, **kwargs):
165 self._log(Log.FATAL, True, fmt, *args, **kwargs)
167 def _log(self, level, use_format, fmt, *args, **kwargs):
168 if self.isEnabledFor(level):
169 frame = inspect.currentframe().f_back # calling method
170 frame = frame.f_back # original log location
171 filename = os.path.split(frame.f_code.co_filename)[1]
172 funcname = frame.f_code.co_name
173 if use_format:
174 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt
175 else:
176 msg = fmt % args if args else fmt
177 if self.UsePythonLogging:
178 levelno = LevelTranslator.lsstLog2logging(level)
179 levelName = logging.getLevelName(levelno)
181 pylog = logging.getLogger(self.getName())
182 record = logging.makeLogRecord(dict(name=self.getName(),
183 levelno=levelno,
184 levelname=levelName,
185 msg=msg,
186 funcName=funcname,
187 filename=filename,
188 pathname=frame.f_code.co_filename,
189 lineno=frame.f_lineno))
190 pylog.handle(record)
191 else:
192 self.logMsg(level, filename, funcname, frame.f_lineno, msg)
194 def __reduce__(self):
195 """Implement pickle support.
196 """
197 args = (self.getName(), )
198 # method has to be module-level, not class method
199 return (getLogger, args)
201 def __repr__(self):
202 # Match python logging style.
203 cls = type(self)
204 class_name = f"{cls.__module__}.{cls.__qualname__}"
205 prefix = "lsst.log.log.log"
206 if class_name.startswith(prefix):
207 class_name = class_name.replace(prefix, "lsst.log")
208 return f"<{class_name} '{self.name}' ({getLevelName(self.getEffectiveLevel())})>"
211class MDCDict(dict):
212 """Dictionary for MDC data.
214 This is internal class used for better formatting of MDC in Python logging
215 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and
216 ``__repr__`` method to produce output better suited for logging records.
217 """
218 def __getitem__(self, name: str):
219 """Returns value for a given key or empty string for missing key.
220 """
221 return self.get(name, "")
223 def __str__(self):
224 """Return string representation, strings are interpolated without
225 quotes.
226 """
227 items = (f"{k}={self[k]}" for k in sorted(self))
228 return "{" + ", ".join(items) + "}"
230 def __repr__(self):
231 return str(self)
234# Export static functions from Log class to module namespace
237def configure(*args):
238 Log.configure(*args)
241def configure_prop(properties):
242 Log.configure_prop(properties)
245def configure_pylog_MDC(level: str, MDC_class: Optional[type] = MDCDict):
246 """Configure log4cxx to send messages to Python logging, with MDC support.
248 Parameters
249 ----------
250 level : `str`
251 Name of the logging level for root log4cxx logger.
252 MDC_class : `type`, optional
253 Type of dictionary which is added to `logging.LogRecord` as an ``MDC``
254 attribute. Any dictionary or ``defaultdict``-like class can be used as
255 a type. If `None` the `logging.LogRecord` will not be augmented.
257 Notes
258 -----
259 This method does two things:
261 - Configures log4cxx with a given logging level and a ``PyLogAppender``
262 appender class which forwards all messages to Python `logging`.
263 - Installs a record factory for Python `logging` that adds ``MDC``
264 attribute to every `logging.LogRecord` object (instance of
265 ``MDC_class``). This will happen by default but can be disabled
266 by setting the ``MDC_class`` parameter to `None`.
267 """
268 if MDC_class is not None:
269 old_factory = logging.getLogRecordFactory()
271 def record_factory(*args, **kwargs):
272 record = old_factory(*args, **kwargs)
273 record.MDC = MDC_class()
274 return record
276 logging.setLogRecordFactory(record_factory)
278 properties = """\
279log4j.rootLogger = {}, PyLog
280log4j.appender.PyLog = PyLogAppender
281""".format(level)
282 configure_prop(properties)
285def getDefaultLogger():
286 return Log.getDefaultLogger()
289def getLogger(loggername):
290 return Log.getLogger(loggername)
293def MDC(key, value):
294 return Log.MDC(key, str(value))
297def MDCRemove(key):
298 Log.MDCRemove(key)
301def MDCRegisterInit(func):
302 Log.MDCRegisterInit(func)
305def setLevel(loggername, level):
306 Log.getLogger(loggername).setLevel(level)
309def getLevel(loggername):
310 return Log.getLogger(loggername).getLevel()
313def getEffectiveLevel(loggername):
314 return Log.getLogger(loggername).getEffectiveLevel()
317def isEnabledFor(loggername, level):
318 return Log.getLogger(loggername).isEnabledFor(level)
321# This will cause a warning in Sphinx documentation due to confusion between
322# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but
323# note that this does not seem to be Mac-only).
324def log(loggername, level, fmt, *args, **kwargs):
325 Log.getLogger(loggername)._log(level, False, fmt, *args)
328def trace(fmt, *args):
329 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
332def debug(fmt, *args):
333 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
336def info(fmt, *args):
337 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
340def warn(fmt, *args):
341 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
344def warning(fmt, *args):
345 warn(fmt, *args)
348def error(fmt, *args):
349 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
352def fatal(fmt, *args):
353 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
356def critical(fmt, *args):
357 fatal(fmt, *args)
360def logf(loggername, level, fmt, *args, **kwargs):
361 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs)
364def tracef(fmt, *args, **kwargs):
365 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs)
368def debugf(fmt, *args, **kwargs):
369 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs)
372def infof(fmt, *args, **kwargs):
373 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs)
376def warnf(fmt, *args, **kwargs):
377 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs)
380def errorf(fmt, *args, **kwargs):
381 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs)
384def fatalf(fmt, *args, **kwargs):
385 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs)
388def lwpID():
389 return Log.lwpID
392def getLevelName(level):
393 """Return the name associated with this logging level.
395 Returns "Level %d" if no name can be found.
396 """
397 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR")
398 for name in names:
399 test_level = getattr(Log, name)
400 if test_level == level:
401 return name
402 return f"Level {level}"
405# This will cause a warning in Sphinx documentation due to confusion between
406# UsePythonLogging and usePythonLogging.
407# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
408# does not seem to be Mac-only).
409def usePythonLogging():
410 Log.usePythonLogging()
413def doNotUsePythonLogging():
414 Log.doNotUsePythonLogging()
417class UsePythonLogging:
418 """Context manager to enable Python log forwarding temporarily.
419 """
421 def __init__(self):
422 self.current = Log.UsePythonLogging
424 def __enter__(self):
425 Log.usePythonLogging()
427 def __exit__(self, exc_type, exc_value, traceback):
428 Log.UsePythonLogging = self.current
431class LevelTranslator:
432 """Helper class to translate levels between ``lsst.log`` and Python
433 `logging`.
434 """
435 @staticmethod
436 def lsstLog2logging(level):
437 """Translates from lsst.log/log4cxx levels to `logging` module levels.
439 Parameters
440 ----------
441 level : `int`
442 Logging level number used by `lsst.log`, typically one of the
443 constants defined in this module (`DEBUG`, `INFO`, etc.)
445 Returns
446 -------
447 level : `int`
448 Correspoding logging level number for Python `logging` module.
449 """
450 # Python logging levels are same as lsst.log divided by 1000,
451 # logging does not have TRACE level by default but it is OK to use
452 # that numeric level and we may even add TRACE later.
453 return level//1000
455 @staticmethod
456 def logging2lsstLog(level):
457 """Translates from standard python `logging` module levels to
458 lsst.log/log4cxx levels.
460 Parameters
461 ----------
462 level : `int`
463 Logging level number used by Python `logging`, typically one of
464 the constants defined by `logging` module (`logging.DEBUG`,
465 `logging.INFO`, etc.)
467 Returns
468 -------
469 level : `int`
470 Correspoding logging level number for `lsst.log` module.
471 """
472 return level*1000
475class LogHandler(logging.Handler):
476 """Handler for Python logging module that emits to LSST logging.
478 Parameters
479 ----------
480 level : `int`
481 Level at which to set the this handler.
483 Notes
484 -----
485 If this handler is enabled and `lsst.log` has been configured to use
486 Python `logging`, the handler will do nothing itself if any other
487 handler has been registered with the Python logger. If it does not
488 think that anything else is handling the message it will attempt to
489 send the message via a default `~logging.StreamHandler`. The safest
490 approach is to configure the logger with an additional handler
491 (possibly the ROOT logger) if `lsst.log` is to be configured to use
492 Python logging.
493 """
495 def __init__(self, level=logging.NOTSET):
496 logging.Handler.__init__(self, level=level)
497 # Format as a simple message because lsst.log will format the
498 # message a second time.
499 self.formatter = logging.Formatter(fmt="%(message)s")
501 def handle(self, record):
502 logger = Log.getLogger(record.name)
503 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
504 logging.Handler.handle(self, record)
506 def emit(self, record):
507 if Log.UsePythonLogging:
508 # Do not forward this message to lsst.log since this may cause
509 # a logging loop.
511 # Work out whether any other handler is going to be invoked
512 # for this logger.
513 pylgr = logging.getLogger(record.name)
515 # If another handler is registered that is not LogHandler
516 # we ignore this request
517 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
518 return
520 # If the parent has handlers and propagation is enabled
521 # we punt as well (and if a LogHandler is involved then we will
522 # ask the same question when we get to it).
523 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
524 return
526 # Force this message to appear somewhere.
527 # If something else should happen then the caller should add a
528 # second Handler.
529 stream = logging.StreamHandler()
530 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
531 stream.handle(record)
532 return
534 logger = Log.getLogger(record.name)
535 # Use standard formatting class to format message part of the record
536 message = self.formatter.format(record)
538 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
539 record.filename, record.funcName,
540 record.lineno, message)