Coverage for python/lsst/log/log/logContinued.py: 42%
223 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 12:36 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 12:36 -0800
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 # Do not call warn() because that will result in an incorrect
121 # line number in the log.
122 self._log(Log.WARN, False, fmt, *args)
124 def error(self, fmt, *args):
125 self._log(Log.ERROR, False, fmt, *args)
127 def fatal(self, fmt, *args):
128 self._log(Log.FATAL, False, fmt, *args)
130 def critical(self, fmt, *args):
131 # Do not call fatal() because that will result in an incorrect
132 # line number in the log.
133 self._log(Log.FATAL, False, fmt, *args)
135 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
136 " Will be removed after v25",
137 version="v23.0", category=FutureWarning)
138 def tracef(self, fmt, *args, **kwargs):
139 self._log(Log.TRACE, True, fmt, *args, **kwargs)
141 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
142 " Will be removed after v25",
143 version="v23.0", category=FutureWarning)
144 def debugf(self, fmt, *args, **kwargs):
145 self._log(Log.DEBUG, True, fmt, *args, **kwargs)
147 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
148 " Will be removed after v25",
149 version="v23.0", category=FutureWarning)
150 def infof(self, fmt, *args, **kwargs):
151 self._log(Log.INFO, True, fmt, *args, **kwargs)
153 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
154 " Will be removed after v25",
155 version="v23.0", category=FutureWarning)
156 def warnf(self, fmt, *args, **kwargs):
157 self._log(Log.WARN, True, fmt, *args, **kwargs)
159 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
160 " Will be removed after v25",
161 version="v23.0", category=FutureWarning)
162 def errorf(self, fmt, *args, **kwargs):
163 self._log(Log.ERROR, True, fmt, *args, **kwargs)
165 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
166 " Will be removed after v25",
167 version="v23.0", category=FutureWarning)
168 def fatalf(self, fmt, *args, **kwargs):
169 self._log(Log.FATAL, True, fmt, *args, **kwargs)
171 def _log(self, level, use_format, fmt, *args, **kwargs):
172 if self.isEnabledFor(level):
173 frame = inspect.currentframe().f_back # calling method
174 frame = frame.f_back # original log location
175 filename = os.path.split(frame.f_code.co_filename)[1]
176 funcname = frame.f_code.co_name
177 if use_format:
178 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt
179 else:
180 msg = fmt % args if args else fmt
181 if self.UsePythonLogging:
182 levelno = LevelTranslator.lsstLog2logging(level)
183 levelName = logging.getLevelName(levelno)
185 pylog = logging.getLogger(self.getName())
186 record = logging.makeLogRecord(dict(name=self.getName(),
187 levelno=levelno,
188 levelname=levelName,
189 msg=msg,
190 funcName=funcname,
191 filename=filename,
192 pathname=frame.f_code.co_filename,
193 lineno=frame.f_lineno))
194 pylog.handle(record)
195 else:
196 self.logMsg(level, filename, funcname, frame.f_lineno, msg)
198 def __reduce__(self):
199 """Implement pickle support.
200 """
201 args = (self.getName(), )
202 # method has to be module-level, not class method
203 return (getLogger, args)
205 def __repr__(self):
206 # Match python logging style.
207 cls = type(self)
208 class_name = f"{cls.__module__}.{cls.__qualname__}"
209 prefix = "lsst.log.log.log"
210 if class_name.startswith(prefix):
211 class_name = class_name.replace(prefix, "lsst.log")
212 return f"<{class_name} '{self.name}' ({getLevelName(self.getEffectiveLevel())})>"
215class MDCDict(dict):
216 """Dictionary for MDC data.
218 This is internal class used for better formatting of MDC in Python logging
219 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and
220 ``__repr__`` method to produce output better suited for logging records.
221 """
222 def __getitem__(self, name: str):
223 """Returns value for a given key or empty string for missing key.
224 """
225 return self.get(name, "")
227 def __str__(self):
228 """Return string representation, strings are interpolated without
229 quotes.
230 """
231 items = (f"{k}={self[k]}" for k in sorted(self))
232 return "{" + ", ".join(items) + "}"
234 def __repr__(self):
235 return str(self)
238# Export static functions from Log class to module namespace
241def configure(*args):
242 Log.configure(*args)
245def configure_prop(properties):
246 Log.configure_prop(properties)
249def configure_pylog_MDC(level: str, MDC_class: Optional[type] = MDCDict):
250 """Configure log4cxx to send messages to Python logging, with MDC support.
252 Parameters
253 ----------
254 level : `str`
255 Name of the logging level for root log4cxx logger.
256 MDC_class : `type`, optional
257 Type of dictionary which is added to `logging.LogRecord` as an ``MDC``
258 attribute. Any dictionary or ``defaultdict``-like class can be used as
259 a type. If `None` the `logging.LogRecord` will not be augmented.
261 Notes
262 -----
263 This method does two things:
265 - Configures log4cxx with a given logging level and a ``PyLogAppender``
266 appender class which forwards all messages to Python `logging`.
267 - Installs a record factory for Python `logging` that adds ``MDC``
268 attribute to every `logging.LogRecord` object (instance of
269 ``MDC_class``). This will happen by default but can be disabled
270 by setting the ``MDC_class`` parameter to `None`.
271 """
272 if MDC_class is not None:
273 old_factory = logging.getLogRecordFactory()
275 def record_factory(*args, **kwargs):
276 record = old_factory(*args, **kwargs)
277 record.MDC = MDC_class()
278 return record
280 logging.setLogRecordFactory(record_factory)
282 properties = """\
283log4j.rootLogger = {}, PyLog
284log4j.appender.PyLog = PyLogAppender
285""".format(level)
286 configure_prop(properties)
289def getDefaultLogger():
290 return Log.getDefaultLogger()
293def getLogger(loggername):
294 return Log.getLogger(loggername)
297def MDC(key, value):
298 return Log.MDC(key, str(value))
301def MDCRemove(key):
302 Log.MDCRemove(key)
305def MDCRegisterInit(func):
306 Log.MDCRegisterInit(func)
309def setLevel(loggername, level):
310 Log.getLogger(loggername).setLevel(level)
313def getLevel(loggername):
314 return Log.getLogger(loggername).getLevel()
317def getEffectiveLevel(loggername):
318 return Log.getLogger(loggername).getEffectiveLevel()
321def isEnabledFor(loggername, level):
322 return Log.getLogger(loggername).isEnabledFor(level)
325# This will cause a warning in Sphinx documentation due to confusion between
326# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but
327# note that this does not seem to be Mac-only).
328def log(loggername, level, fmt, *args, **kwargs):
329 Log.getLogger(loggername)._log(level, False, fmt, *args)
332def trace(fmt, *args):
333 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
336def debug(fmt, *args):
337 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
340def info(fmt, *args):
341 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
344def warn(fmt, *args):
345 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
348def warning(fmt, *args):
349 warn(fmt, *args)
352def error(fmt, *args):
353 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
356def fatal(fmt, *args):
357 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
360def critical(fmt, *args):
361 fatal(fmt, *args)
364def logf(loggername, level, fmt, *args, **kwargs):
365 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs)
368def tracef(fmt, *args, **kwargs):
369 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs)
372def debugf(fmt, *args, **kwargs):
373 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs)
376def infof(fmt, *args, **kwargs):
377 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs)
380def warnf(fmt, *args, **kwargs):
381 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs)
384def errorf(fmt, *args, **kwargs):
385 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs)
388def fatalf(fmt, *args, **kwargs):
389 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs)
392def lwpID():
393 return Log.lwpID
396def getLevelName(level):
397 """Return the name associated with this logging level.
399 Returns "Level %d" if no name can be found.
400 """
401 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR")
402 for name in names:
403 test_level = getattr(Log, name)
404 if test_level == level:
405 return name
406 return f"Level {level}"
409# This will cause a warning in Sphinx documentation due to confusion between
410# UsePythonLogging and usePythonLogging.
411# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
412# does not seem to be Mac-only).
413def usePythonLogging():
414 Log.usePythonLogging()
417def doNotUsePythonLogging():
418 Log.doNotUsePythonLogging()
421class UsePythonLogging:
422 """Context manager to enable Python log forwarding temporarily.
423 """
425 def __init__(self):
426 self.current = Log.UsePythonLogging
428 def __enter__(self):
429 Log.usePythonLogging()
431 def __exit__(self, exc_type, exc_value, traceback):
432 Log.UsePythonLogging = self.current
435class LevelTranslator:
436 """Helper class to translate levels between ``lsst.log`` and Python
437 `logging`.
438 """
439 @staticmethod
440 def lsstLog2logging(level):
441 """Translates from lsst.log/log4cxx levels to `logging` module levels.
443 Parameters
444 ----------
445 level : `int`
446 Logging level number used by `lsst.log`, typically one of the
447 constants defined in this module (`DEBUG`, `INFO`, etc.)
449 Returns
450 -------
451 level : `int`
452 Correspoding logging level number for Python `logging` module.
453 """
454 # Python logging levels are same as lsst.log divided by 1000,
455 # logging does not have TRACE level by default but it is OK to use
456 # that numeric level and we may even add TRACE later.
457 return level//1000
459 @staticmethod
460 def logging2lsstLog(level):
461 """Translates from standard python `logging` module levels to
462 lsst.log/log4cxx levels.
464 Parameters
465 ----------
466 level : `int`
467 Logging level number used by Python `logging`, typically one of
468 the constants defined by `logging` module (`logging.DEBUG`,
469 `logging.INFO`, etc.)
471 Returns
472 -------
473 level : `int`
474 Correspoding logging level number for `lsst.log` module.
475 """
476 return level*1000
479class LogHandler(logging.Handler):
480 """Handler for Python logging module that emits to LSST logging.
482 Parameters
483 ----------
484 level : `int`
485 Level at which to set the this handler.
487 Notes
488 -----
489 If this handler is enabled and `lsst.log` has been configured to use
490 Python `logging`, the handler will do nothing itself if any other
491 handler has been registered with the Python logger. If it does not
492 think that anything else is handling the message it will attempt to
493 send the message via a default `~logging.StreamHandler`. The safest
494 approach is to configure the logger with an additional handler
495 (possibly the ROOT logger) if `lsst.log` is to be configured to use
496 Python logging.
497 """
499 def __init__(self, level=logging.NOTSET):
500 logging.Handler.__init__(self, level=level)
501 # Format as a simple message because lsst.log will format the
502 # message a second time.
503 self.formatter = logging.Formatter(fmt="%(message)s")
505 def handle(self, record):
506 logger = Log.getLogger(record.name)
507 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
508 logging.Handler.handle(self, record)
510 def emit(self, record):
511 if Log.UsePythonLogging:
512 # Do not forward this message to lsst.log since this may cause
513 # a logging loop.
515 # Work out whether any other handler is going to be invoked
516 # for this logger.
517 pylgr = logging.getLogger(record.name)
519 # If another handler is registered that is not LogHandler
520 # we ignore this request
521 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
522 return
524 # If the parent has handlers and propagation is enabled
525 # we punt as well (and if a LogHandler is involved then we will
526 # ask the same question when we get to it).
527 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
528 return
530 # Force this message to appear somewhere.
531 # If something else should happen then the caller should add a
532 # second Handler.
533 stream = logging.StreamHandler()
534 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
535 stream.handle(record)
536 return
538 logger = Log.getLogger(record.name)
539 # Use standard formatting class to format message part of the record
540 message = self.formatter.format(record)
542 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
543 record.filename, record.funcName,
544 record.lineno, message)