Coverage for python/lsst/log/log/logContinued.py: 40%
204 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-11 10:25 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-11 10:25 +0000
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
39from lsst.utils import continueClass
41from .log import Log
43TRACE = 5000
44DEBUG = 10000
45INFO = 20000
46WARN = 30000
47ERROR = 40000
48FATAL = 50000
50# For compatibility with python logging
51CRITICAL = FATAL
52WARNING = WARN
55@continueClass # noqa: F811 (FIXME: remove for py 3.8+)
56class Log: # noqa: F811
57 UsePythonLogging = False
58 """Forward Python `lsst.log` messages to Python `logging` package."""
60 CRITICAL = CRITICAL
61 WARNING = WARNING
63 @classmethod
64 def usePythonLogging(cls):
65 """Forward log messages to Python `logging`
67 Notes
68 -----
69 This is useful for unit testing when you want to ensure
70 that log messages are captured by the testing environment
71 as distinct from standard output.
73 This state only affects messages sent to the `lsst.log`
74 package from Python.
75 """
76 cls.UsePythonLogging = True
78 @classmethod
79 def doNotUsePythonLogging(cls):
80 """Forward log messages to LSST logging system.
82 Notes
83 -----
84 This is the default state.
85 """
86 cls.UsePythonLogging = False
88 @property
89 def name(self):
90 return self.getName()
92 @property
93 def level(self):
94 return self.getLevel()
96 @property
97 def parent(self):
98 """Returns the parent logger, or None if this is the root logger."""
99 if not self.name:
100 return None
101 parent_name = self.name.rpartition(".")[0]
102 if not parent_name:
103 return self.getDefaultLogger()
104 return self.getLogger(parent_name)
106 def trace(self, fmt, *args):
107 self._log(Log.TRACE, False, fmt, *args)
109 def debug(self, fmt, *args):
110 self._log(Log.DEBUG, False, fmt, *args)
112 def info(self, fmt, *args):
113 self._log(Log.INFO, False, fmt, *args)
115 def warn(self, fmt, *args):
116 self._log(Log.WARN, False, fmt, *args)
118 def warning(self, fmt, *args):
119 # Do not call warn() because that will result in an incorrect
120 # line number in the log.
121 self._log(Log.WARN, False, fmt, *args)
123 def error(self, fmt, *args):
124 self._log(Log.ERROR, False, fmt, *args)
126 def fatal(self, fmt, *args):
127 self._log(Log.FATAL, False, fmt, *args)
129 def critical(self, fmt, *args):
130 # Do not call fatal() because that will result in an incorrect
131 # line number in the log.
132 self._log(Log.FATAL, False, fmt, *args)
134 def _log(self, level, use_format, fmt, *args, **kwargs):
135 if self.isEnabledFor(level):
136 frame = inspect.currentframe().f_back # calling method
137 frame = frame.f_back # original log location
138 filename = os.path.split(frame.f_code.co_filename)[1]
139 funcname = frame.f_code.co_name
140 if use_format:
141 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt
142 else:
143 msg = fmt % args if args else fmt
144 if self.UsePythonLogging:
145 levelno = LevelTranslator.lsstLog2logging(level)
146 levelName = logging.getLevelName(levelno)
148 pylog = logging.getLogger(self.getName())
149 record = logging.makeLogRecord(dict(name=self.getName(),
150 levelno=levelno,
151 levelname=levelName,
152 msg=msg,
153 funcName=funcname,
154 filename=filename,
155 pathname=frame.f_code.co_filename,
156 lineno=frame.f_lineno))
157 pylog.handle(record)
158 else:
159 self.logMsg(level, filename, funcname, frame.f_lineno, msg)
161 def __reduce__(self):
162 """Implement pickle support.
163 """
164 args = (self.getName(), )
165 # method has to be module-level, not class method
166 return (getLogger, args)
168 def __repr__(self):
169 # Match python logging style.
170 cls = type(self)
171 class_name = f"{cls.__module__}.{cls.__qualname__}"
172 prefix = "lsst.log.log.log"
173 if class_name.startswith(prefix):
174 class_name = class_name.replace(prefix, "lsst.log")
175 return f"<{class_name} '{self.name}' ({getLevelName(self.getEffectiveLevel())})>"
178class MDCDict(dict):
179 """Dictionary for MDC data.
181 This is internal class used for better formatting of MDC in Python logging
182 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and
183 ``__repr__`` method to produce output better suited for logging records.
184 """
185 def __getitem__(self, name: str):
186 """Returns value for a given key or empty string for missing key.
187 """
188 return self.get(name, "")
190 def __str__(self):
191 """Return string representation, strings are interpolated without
192 quotes.
193 """
194 items = (f"{k}={self[k]}" for k in sorted(self))
195 return "{" + ", ".join(items) + "}"
197 def __repr__(self):
198 return str(self)
201# Export static functions from Log class to module namespace
204def configure(*args):
205 Log.configure(*args)
208def configure_prop(properties):
209 Log.configure_prop(properties)
212def configure_pylog_MDC(level: str, MDC_class: Optional[type] = MDCDict):
213 """Configure log4cxx to send messages to Python logging, with MDC support.
215 Parameters
216 ----------
217 level : `str`
218 Name of the logging level for root log4cxx logger.
219 MDC_class : `type`, optional
220 Type of dictionary which is added to `logging.LogRecord` as an ``MDC``
221 attribute. Any dictionary or ``defaultdict``-like class can be used as
222 a type. If `None` the `logging.LogRecord` will not be augmented.
224 Notes
225 -----
226 This method does two things:
228 - Configures log4cxx with a given logging level and a ``PyLogAppender``
229 appender class which forwards all messages to Python `logging`.
230 - Installs a record factory for Python `logging` that adds ``MDC``
231 attribute to every `logging.LogRecord` object (instance of
232 ``MDC_class``). This will happen by default but can be disabled
233 by setting the ``MDC_class`` parameter to `None`.
234 """
235 if MDC_class is not None:
236 old_factory = logging.getLogRecordFactory()
238 def record_factory(*args, **kwargs):
239 record = old_factory(*args, **kwargs)
240 record.MDC = MDC_class()
241 return record
243 logging.setLogRecordFactory(record_factory)
245 properties = """\
246log4j.rootLogger = {}, PyLog
247log4j.appender.PyLog = PyLogAppender
248""".format(level)
249 configure_prop(properties)
252def getDefaultLogger():
253 return Log.getDefaultLogger()
256def getLogger(loggername):
257 return Log.getLogger(loggername)
260def MDC(key, value):
261 return Log.MDC(key, str(value))
264def MDCRemove(key):
265 Log.MDCRemove(key)
268def MDCRegisterInit(func):
269 Log.MDCRegisterInit(func)
272def setLevel(loggername, level):
273 Log.getLogger(loggername).setLevel(level)
276def getLevel(loggername):
277 return Log.getLogger(loggername).getLevel()
280def getEffectiveLevel(loggername):
281 return Log.getLogger(loggername).getEffectiveLevel()
284def isEnabledFor(loggername, level):
285 return Log.getLogger(loggername).isEnabledFor(level)
288# This will cause a warning in Sphinx documentation due to confusion between
289# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but
290# note that this does not seem to be Mac-only).
291def log(loggername, level, fmt, *args, **kwargs):
292 Log.getLogger(loggername)._log(level, False, fmt, *args)
295def trace(fmt, *args):
296 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
299def debug(fmt, *args):
300 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
303def info(fmt, *args):
304 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
307def warn(fmt, *args):
308 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
311def warning(fmt, *args):
312 warn(fmt, *args)
315def error(fmt, *args):
316 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
319def fatal(fmt, *args):
320 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
323def critical(fmt, *args):
324 fatal(fmt, *args)
327def logf(loggername, level, fmt, *args, **kwargs):
328 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs)
331def tracef(fmt, *args, **kwargs):
332 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs)
335def debugf(fmt, *args, **kwargs):
336 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs)
339def infof(fmt, *args, **kwargs):
340 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs)
343def warnf(fmt, *args, **kwargs):
344 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs)
347def errorf(fmt, *args, **kwargs):
348 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs)
351def fatalf(fmt, *args, **kwargs):
352 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs)
355def lwpID():
356 return Log.lwpID
359def getLevelName(level):
360 """Return the name associated with this logging level.
362 Returns "Level %d" if no name can be found.
363 """
364 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR")
365 for name in names:
366 test_level = getattr(Log, name)
367 if test_level == level:
368 return name
369 return f"Level {level}"
372# This will cause a warning in Sphinx documentation due to confusion between
373# UsePythonLogging and usePythonLogging.
374# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
375# does not seem to be Mac-only).
376def usePythonLogging():
377 Log.usePythonLogging()
380def doNotUsePythonLogging():
381 Log.doNotUsePythonLogging()
384class UsePythonLogging:
385 """Context manager to enable Python log forwarding temporarily.
386 """
388 def __init__(self):
389 self.current = Log.UsePythonLogging
391 def __enter__(self):
392 Log.usePythonLogging()
394 def __exit__(self, exc_type, exc_value, traceback):
395 Log.UsePythonLogging = self.current
398class LevelTranslator:
399 """Helper class to translate levels between ``lsst.log`` and Python
400 `logging`.
401 """
402 @staticmethod
403 def lsstLog2logging(level):
404 """Translates from lsst.log/log4cxx levels to `logging` module levels.
406 Parameters
407 ----------
408 level : `int`
409 Logging level number used by `lsst.log`, typically one of the
410 constants defined in this module (`DEBUG`, `INFO`, etc.)
412 Returns
413 -------
414 level : `int`
415 Correspoding logging level number for Python `logging` module.
416 """
417 # Python logging levels are same as lsst.log divided by 1000,
418 # logging does not have TRACE level by default but it is OK to use
419 # that numeric level and we may even add TRACE later.
420 return level//1000
422 @staticmethod
423 def logging2lsstLog(level):
424 """Translates from standard python `logging` module levels to
425 lsst.log/log4cxx levels.
427 Parameters
428 ----------
429 level : `int`
430 Logging level number used by Python `logging`, typically one of
431 the constants defined by `logging` module (`logging.DEBUG`,
432 `logging.INFO`, etc.)
434 Returns
435 -------
436 level : `int`
437 Correspoding logging level number for `lsst.log` module.
438 """
439 return level*1000
442class LogHandler(logging.Handler):
443 """Handler for Python logging module that emits to LSST logging.
445 Parameters
446 ----------
447 level : `int`
448 Level at which to set the this handler.
450 Notes
451 -----
452 If this handler is enabled and `lsst.log` has been configured to use
453 Python `logging`, the handler will do nothing itself if any other
454 handler has been registered with the Python logger. If it does not
455 think that anything else is handling the message it will attempt to
456 send the message via a default `~logging.StreamHandler`. The safest
457 approach is to configure the logger with an additional handler
458 (possibly the ROOT logger) if `lsst.log` is to be configured to use
459 Python logging.
460 """
462 def __init__(self, level=logging.NOTSET):
463 logging.Handler.__init__(self, level=level)
464 # Format as a simple message because lsst.log will format the
465 # message a second time.
466 self.formatter = logging.Formatter(fmt="%(message)s")
468 def handle(self, record):
469 logger = Log.getLogger(record.name)
470 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
471 logging.Handler.handle(self, record)
473 def emit(self, record):
474 if Log.UsePythonLogging:
475 # Do not forward this message to lsst.log since this may cause
476 # a logging loop.
478 # Work out whether any other handler is going to be invoked
479 # for this logger.
480 pylgr = logging.getLogger(record.name)
482 # If another handler is registered that is not LogHandler
483 # we ignore this request
484 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
485 return
487 # If the parent has handlers and propagation is enabled
488 # we punt as well (and if a LogHandler is involved then we will
489 # ask the same question when we get to it).
490 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
491 return
493 # Force this message to appear somewhere.
494 # If something else should happen then the caller should add a
495 # second Handler.
496 stream = logging.StreamHandler()
497 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
498 stream.handle(record)
499 return
501 logger = Log.getLogger(record.name)
502 # Use standard formatting class to format message part of the record
503 message = self.formatter.format(record)
505 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
506 record.filename, record.funcName,
507 record.lineno, message)