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 deprecated.sphinx import deprecated
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 self.warn(fmt, *args)
121 def error(self, fmt, *args):
122 self._log(Log.ERROR, False, fmt, *args)
124 def fatal(self, fmt, *args):
125 self._log(Log.FATAL, False, fmt, *args)
127 def critical(self, fmt, *args):
128 self.fatal(fmt, *args)
130 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
131 " Will be removed after v25",
132 version="v23.0", category=FutureWarning)
133 def tracef(self, fmt, *args, **kwargs):
134 self._log(Log.TRACE, True, fmt, *args, **kwargs)
136 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
137 " Will be removed after v25",
138 version="v23.0", category=FutureWarning)
139 def debugf(self, fmt, *args, **kwargs):
140 self._log(Log.DEBUG, True, fmt, *args, **kwargs)
142 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
143 " Will be removed after v25",
144 version="v23.0", category=FutureWarning)
145 def infof(self, fmt, *args, **kwargs):
146 self._log(Log.INFO, True, fmt, *args, **kwargs)
148 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
149 " Will be removed after v25",
150 version="v23.0", category=FutureWarning)
151 def warnf(self, fmt, *args, **kwargs):
152 self._log(Log.WARN, True, fmt, *args, **kwargs)
154 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
155 " Will be removed after v25",
156 version="v23.0", category=FutureWarning)
157 def errorf(self, fmt, *args, **kwargs):
158 self._log(Log.ERROR, True, fmt, *args, **kwargs)
160 @deprecated(reason="f-string log messages are now deprecated to match python logging convention."
161 " Will be removed after v25",
162 version="v23.0", category=FutureWarning)
163 def fatalf(self, fmt, *args, **kwargs):
164 self._log(Log.FATAL, True, fmt, *args, **kwargs)
166 def _log(self, level, use_format, fmt, *args, **kwargs):
167 if self.isEnabledFor(level):
168 frame = inspect.currentframe().f_back # calling method
169 frame = frame.f_back # original log location
170 filename = os.path.split(frame.f_code.co_filename)[1]
171 funcname = frame.f_code.co_name
172 if use_format:
173 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt
174 else:
175 msg = fmt % args if args else fmt
176 if self.UsePythonLogging:
177 levelno = LevelTranslator.lsstLog2logging(level)
178 levelName = logging.getLevelName(levelno)
180 pylog = logging.getLogger(self.getName())
181 record = logging.makeLogRecord(dict(name=self.getName(),
182 levelno=levelno,
183 levelname=levelName,
184 msg=msg,
185 funcName=funcname,
186 filename=filename,
187 pathname=frame.f_code.co_filename,
188 lineno=frame.f_lineno))
189 pylog.handle(record)
190 else:
191 self.logMsg(level, filename, funcname, frame.f_lineno, msg)
193 def __reduce__(self):
194 """Implement pickle support.
195 """
196 args = (self.getName(), )
197 # method has to be module-level, not class method
198 return (getLogger, args)
200 def __repr__(self):
201 # Match python logging style.
202 cls = type(self)
203 class_name = f"{cls.__module__}.{cls.__qualname__}"
204 prefix = "lsst.log.log.log"
205 if class_name.startswith(prefix):
206 class_name = class_name.replace(prefix, "lsst.log")
207 return f"<{class_name} '{self.name}' ({getLevelName(self.getEffectiveLevel())})>"
210class MDCDict(dict):
211 """Dictionary for MDC data.
213 This is internal class used for better formatting of MDC in Python logging
214 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and
215 ``__repr__`` method to produce output better suited for logging records.
216 """
217 def __getitem__(self, name: str):
218 """Returns value for a given key or empty string for missing key.
219 """
220 return self.get(name, "")
222 def __str__(self):
223 """Return string representation, strings are interpolated without
224 quotes.
225 """
226 items = (f"{k}={self[k]}" for k in sorted(self))
227 return "{" + ", ".join(items) + "}"
229 def __repr__(self):
230 return str(self)
233# Export static functions from Log class to module namespace
236def configure(*args):
237 Log.configure(*args)
240def configure_prop(properties):
241 Log.configure_prop(properties)
244def configure_pylog_MDC(level: str, MDC_class: type = MDCDict):
245 """Configure log4cxx to send messages to Python logging, with MDC support.
247 Parameters
248 ----------
249 level : `str`
250 Name of the logging level for root log4cxx logger.
251 MDC_class : `type`, optional
252 Type of dictionary which is added to `logging.LogRecord` as an ``MDC``
253 attribute. Any dictionary or ``defaultdict``-like class can be used as
254 a type.
256 Notes
257 -----
258 This method does two things:
260 - Configures log4cxx with a given logging level and a ``PyLogAppender``
261 appender class which forwards all messages to Python `logging`.
262 - Installs a record factory for Python `logging` that adds ``MDC``
263 attribute to every `logging.LogRecord` object (instance of
264 ``MDC_class``).
265 """
266 old_factory = logging.getLogRecordFactory()
268 def record_factory(*args, **kwargs):
269 record = old_factory(*args, **kwargs)
270 record.MDC = MDC_class()
271 return record
273 logging.setLogRecordFactory(record_factory)
275 properties = """\
276log4j.rootLogger = {}, PyLog
277log4j.appender.PyLog = PyLogAppender
278""".format(level)
279 configure_prop(properties)
282def getDefaultLogger():
283 return Log.getDefaultLogger()
286def getLogger(loggername):
287 return Log.getLogger(loggername)
290def MDC(key, value):
291 return Log.MDC(key, str(value))
294def MDCRemove(key):
295 Log.MDCRemove(key)
298def MDCRegisterInit(func):
299 Log.MDCRegisterInit(func)
302def setLevel(loggername, level):
303 Log.getLogger(loggername).setLevel(level)
306def getLevel(loggername):
307 return Log.getLogger(loggername).getLevel()
310def getEffectiveLevel(loggername):
311 return Log.getLogger(loggername).getEffectiveLevel()
314def isEnabledFor(loggername, level):
315 return Log.getLogger(loggername).isEnabledFor(level)
318# This will cause a warning in Sphinx documentation due to confusion between
319# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but
320# note that this does not seem to be Mac-only).
321def log(loggername, level, fmt, *args, **kwargs):
322 Log.getLogger(loggername)._log(level, False, fmt, *args)
325def trace(fmt, *args):
326 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
329def debug(fmt, *args):
330 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
333def info(fmt, *args):
334 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
337def warn(fmt, *args):
338 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
341def warning(fmt, *args):
342 warn(fmt, *args)
345def error(fmt, *args):
346 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
349def fatal(fmt, *args):
350 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
353def critical(fmt, *args):
354 fatal(fmt, *args)
357def logf(loggername, level, fmt, *args, **kwargs):
358 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs)
361def tracef(fmt, *args, **kwargs):
362 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs)
365def debugf(fmt, *args, **kwargs):
366 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs)
369def infof(fmt, *args, **kwargs):
370 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs)
373def warnf(fmt, *args, **kwargs):
374 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs)
377def errorf(fmt, *args, **kwargs):
378 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs)
381def fatalf(fmt, *args, **kwargs):
382 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs)
385def lwpID():
386 return Log.lwpID
389def getLevelName(level):
390 """Return the name associated with this logging level.
392 Returns "Level %d" if no name can be found.
393 """
394 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR")
395 for name in names:
396 test_level = getattr(Log, name)
397 if test_level == level:
398 return name
399 return f"Level {level}"
402# This will cause a warning in Sphinx documentation due to confusion between
403# UsePythonLogging and usePythonLogging.
404# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
405# does not seem to be Mac-only).
406def usePythonLogging():
407 Log.usePythonLogging()
410def doNotUsePythonLogging():
411 Log.doNotUsePythonLogging()
414class UsePythonLogging:
415 """Context manager to enable Python log forwarding temporarily.
416 """
418 def __init__(self):
419 self.current = Log.UsePythonLogging
421 def __enter__(self):
422 Log.usePythonLogging()
424 def __exit__(self, exc_type, exc_value, traceback):
425 Log.UsePythonLogging = self.current
428class LevelTranslator:
429 """Helper class to translate levels between ``lsst.log`` and Python
430 `logging`.
431 """
432 @staticmethod
433 def lsstLog2logging(level):
434 """Translates from lsst.log/log4cxx levels to `logging` module levels.
436 Parameters
437 ----------
438 level : `int`
439 Logging level number used by `lsst.log`, typically one of the
440 constants defined in this module (`DEBUG`, `INFO`, etc.)
442 Returns
443 -------
444 level : `int`
445 Correspoding logging level number for Python `logging` module.
446 """
447 # Python logging levels are same as lsst.log divided by 1000,
448 # logging does not have TRACE level by default but it is OK to use
449 # that numeric level and we may even add TRACE later.
450 return level//1000
452 @staticmethod
453 def logging2lsstLog(level):
454 """Translates from standard python `logging` module levels to
455 lsst.log/log4cxx levels.
457 Parameters
458 ----------
459 level : `int`
460 Logging level number used by Python `logging`, typically one of
461 the constants defined by `logging` module (`logging.DEBUG`,
462 `logging.INFO`, etc.)
464 Returns
465 -------
466 level : `int`
467 Correspoding logging level number for `lsst.log` module.
468 """
469 return level*1000
472class LogHandler(logging.Handler):
473 """Handler for Python logging module that emits to LSST logging.
475 Parameters
476 ----------
477 level : `int`
478 Level at which to set the this handler.
480 Notes
481 -----
482 If this handler is enabled and `lsst.log` has been configured to use
483 Python `logging`, the handler will do nothing itself if any other
484 handler has been registered with the Python logger. If it does not
485 think that anything else is handling the message it will attempt to
486 send the message via a default `~logging.StreamHandler`. The safest
487 approach is to configure the logger with an additional handler
488 (possibly the ROOT logger) if `lsst.log` is to be configured to use
489 Python logging.
490 """
492 def __init__(self, level=logging.NOTSET):
493 logging.Handler.__init__(self, level=level)
494 # Format as a simple message because lsst.log will format the
495 # message a second time.
496 self.formatter = logging.Formatter(fmt="%(message)s")
498 def handle(self, record):
499 logger = Log.getLogger(record.name)
500 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
501 logging.Handler.handle(self, record)
503 def emit(self, record):
504 if Log.UsePythonLogging:
505 # Do not forward this message to lsst.log since this may cause
506 # a logging loop.
508 # Work out whether any other handler is going to be invoked
509 # for this logger.
510 pylgr = logging.getLogger(record.name)
512 # If another handler is registered that is not LogHandler
513 # we ignore this request
514 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
515 return
517 # If the parent has handlers and propagation is enabled
518 # we punt as well (and if a LogHandler is involved then we will
519 # ask the same question when we get to it).
520 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
521 return
523 # Force this message to appear somewhere.
524 # If something else should happen then the caller should add a
525 # second Handler.
526 stream = logging.StreamHandler()
527 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
528 stream.handle(record)
529 return
531 logger = Log.getLogger(record.name)
532 # Use standard formatting class to format message part of the record
533 message = self.formatter.format(record)
535 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
536 record.filename, record.funcName,
537 record.lineno, message)