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

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",
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", "logf", "tracef", "debugf", "infof", "warnf", "errorf", "fatalf",
30 "lwpID", "usePythonLogging", "doNotUsePythonLogging", "UsePythonLogging",
31 "LevelTranslator", "LogHandler"]
33import logging
34import inspect
35import os
37from lsst.utils import continueClass
39from .log import Log
41TRACE = 5000
42DEBUG = 10000
43INFO = 20000
44WARN = 30000
45ERROR = 40000
46FATAL = 50000
49@continueClass # noqa: F811 (FIXME: remove for py 3.8+)
50class Log: # noqa: F811
51 UsePythonLogging = False
52 """Forward Python `lsst.log` messages to Python `logging` package."""
54 @classmethod
55 def usePythonLogging(cls):
56 """Forward log messages to Python `logging`
58 Notes
59 -----
60 This is useful for unit testing when you want to ensure
61 that log messages are captured by the testing environment
62 as distinct from standard output.
64 This state only affects messages sent to the `lsst.log`
65 package from Python.
66 """
67 cls.UsePythonLogging = True
69 @classmethod
70 def doNotUsePythonLogging(cls):
71 """Forward log messages to LSST logging system.
73 Notes
74 -----
75 This is the default state.
76 """
77 cls.UsePythonLogging = False
79 def trace(self, fmt, *args):
80 self._log(Log.TRACE, False, fmt, *args)
82 def debug(self, fmt, *args):
83 self._log(Log.DEBUG, False, fmt, *args)
85 def info(self, fmt, *args):
86 self._log(Log.INFO, False, fmt, *args)
88 def warn(self, fmt, *args):
89 self._log(Log.WARN, False, fmt, *args)
91 def warning(self, fmt, *args):
92 self.warn(fmt, *args)
94 def error(self, fmt, *args):
95 self._log(Log.ERROR, False, fmt, *args)
97 def fatal(self, fmt, *args):
98 self._log(Log.FATAL, False, fmt, *args)
100 def tracef(self, fmt, *args, **kwargs):
101 self._log(Log.TRACE, True, fmt, *args, **kwargs)
103 def debugf(self, fmt, *args, **kwargs):
104 self._log(Log.DEBUG, True, fmt, *args, **kwargs)
106 def infof(self, fmt, *args, **kwargs):
107 self._log(Log.INFO, True, fmt, *args, **kwargs)
109 def warnf(self, fmt, *args, **kwargs):
110 self._log(Log.WARN, True, fmt, *args, **kwargs)
112 def errorf(self, fmt, *args, **kwargs):
113 self._log(Log.ERROR, True, fmt, *args, **kwargs)
115 def fatalf(self, fmt, *args, **kwargs):
116 self._log(Log.FATAL, True, fmt, *args, **kwargs)
118 def _log(self, level, use_format, fmt, *args, **kwargs):
119 if self.isEnabledFor(level):
120 frame = inspect.currentframe().f_back # calling method
121 frame = frame.f_back # original log location
122 filename = os.path.split(frame.f_code.co_filename)[1]
123 funcname = frame.f_code.co_name
124 if use_format:
125 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt
126 else:
127 msg = fmt % args if args else fmt
128 if self.UsePythonLogging:
129 pylog = logging.getLogger(self.getName())
130 record = logging.LogRecord(self.getName(), LevelTranslator.lsstLog2logging(level),
131 filename, frame.f_lineno, msg, None, False, func=funcname)
132 pylog.handle(record)
133 else:
134 self.logMsg(level, filename, funcname, frame.f_lineno, msg)
136 def __reduce__(self):
137 """Implement pickle support.
138 """
139 args = (self.getName(), )
140 # method has to be module-level, not class method
141 return (getLogger, args)
144class MDCDict(dict):
145 """Dictionary for MDC data.
147 This is internal class used for better formatting of MDC in Python logging
148 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and
149 ``__repr__`` method to produce output better suited for logging records.
150 """
151 def __getitem__(self, name: str):
152 """Returns value for a given key or empty string for missing key.
153 """
154 return self.get(name, "")
156 def __str__(self):
157 """Return string representation, strings are interpolated without
158 quotes.
159 """
160 items = (f"{k}={self[k]}" for k in sorted(self))
161 return "{" + ", ".join(items) + "}"
163 def __repr__(self):
164 return str(self)
167# Export static functions from Log class to module namespace
170def configure(*args):
171 Log.configure(*args)
174def configure_prop(properties):
175 Log.configure_prop(properties)
178def configure_pylog_MDC(level: str, MDC_class: type = MDCDict):
179 """Configure log4cxx to send messages to Python logging, with MDC support.
181 Parameters
182 ----------
183 level : `str`
184 Name of the logging level for root log4cxx logger.
185 MDC_class : `type`, optional
186 Type of dictionary which is added to `logging.LogRecord` as an ``MDC``
187 attribute. Any dictionary or ``defaultdict``-like class can be used as
188 a type.
190 Notes
191 -----
192 This method does two things:
194 - Configures log4cxx with a given logging level and a ``PyLogAppender``
195 appender class which forwards all messages to Python `logging`.
196 - Installs a record factory for Python `logging` that adds ``MDC``
197 attribute to every `logging.LogRecord` object (instance of
198 ``MDC_class``).
199 """
200 old_factory = logging.getLogRecordFactory()
202 def record_factory(*args, **kwargs):
203 record = old_factory(*args, **kwargs)
204 record.MDC = MDC_class()
205 return record
207 logging.setLogRecordFactory(record_factory)
209 properties = """\
210log4j.rootLogger = {}, PyLog
211log4j.appender.PyLog = PyLogAppender
212""".format(level)
213 configure_prop(properties)
216def getDefaultLogger():
217 return Log.getDefaultLogger()
220def getLogger(loggername):
221 return Log.getLogger(loggername)
224def MDC(key, value):
225 Log.MDC(key, str(value))
228def MDCRemove(key):
229 Log.MDCRemove(key)
232def MDCRegisterInit(func):
233 Log.MDCRegisterInit(func)
236def setLevel(loggername, level):
237 Log.getLogger(loggername).setLevel(level)
240def getLevel(loggername):
241 Log.getLogger(loggername).getLevel()
244def isEnabledFor(logger, level):
245 Log.getLogger(logger).isEnabledFor(level)
248# This will cause a warning in Sphinx documentation due to confusion between
249# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but
250# note that this does not seem to be Mac-only).
251def log(loggername, level, fmt, *args, **kwargs):
252 Log.getLogger(loggername)._log(level, False, fmt, *args)
255def trace(fmt, *args):
256 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
259def debug(fmt, *args):
260 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
263def info(fmt, *args):
264 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
267def warn(fmt, *args):
268 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
271def warning(fmt, *args):
272 warn(fmt, *args)
275def error(fmt, *args):
276 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
279def fatal(fmt, *args):
280 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
283def logf(loggername, level, fmt, *args, **kwargs):
284 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs)
287def tracef(fmt, *args, **kwargs):
288 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs)
291def debugf(fmt, *args, **kwargs):
292 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs)
295def infof(fmt, *args, **kwargs):
296 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs)
299def warnf(fmt, *args, **kwargs):
300 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs)
303def errorf(fmt, *args, **kwargs):
304 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs)
307def fatalf(fmt, *args, **kwargs):
308 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs)
311def lwpID():
312 return Log.lwpID
315# This will cause a warning in Sphinx documentation due to confusion between
316# UsePythonLogging and usePythonLogging.
317# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
318# does not seem to be Mac-only).
319def usePythonLogging():
320 Log.usePythonLogging()
323def doNotUsePythonLogging():
324 Log.doNotUsePythonLogging()
327class UsePythonLogging:
328 """Context manager to enable Python log forwarding temporarily.
329 """
331 def __init__(self):
332 self.current = Log.UsePythonLogging
334 def __enter__(self):
335 Log.usePythonLogging()
337 def __exit__(self, exc_type, exc_value, traceback):
338 Log.UsePythonLogging = self.current
341class LevelTranslator:
342 """Helper class to translate levels between ``lsst.log`` and Python
343 `logging`.
344 """
345 @staticmethod
346 def lsstLog2logging(level):
347 """Translates from lsst.log/log4cxx levels to `logging` module levels.
349 Parameters
350 ----------
351 level : `int`
352 Logging level number used by `lsst.log`, typically one of the
353 constants defined in this module (`DEBUG`, `INFO`, etc.)
355 Returns
356 -------
357 level : `int`
358 Correspoding logging level number for Python `logging` module.
359 """
360 # Python logging levels are same as lsst.log divided by 1000,
361 # logging does not have TRACE level by default but it is OK to use
362 # that numeric level and we may even add TRACE later.
363 return level//1000
365 @staticmethod
366 def logging2lsstLog(level):
367 """Translates from standard python `logging` module levels to
368 lsst.log/log4cxx levels.
370 Parameters
371 ----------
372 level : `int`
373 Logging level number used by Python `logging`, typically one of
374 the constants defined by `logging` module (`logging.DEBUG`,
375 `logging.INFO`, etc.)
377 Returns
378 -------
379 level : `int`
380 Correspoding logging level number for `lsst.log` module.
381 """
382 return level*1000
385class LogHandler(logging.Handler):
386 """Handler for Python logging module that emits to LSST logging.
388 Parameters
389 ----------
390 level : `int`
391 Level at which to set the this handler.
393 Notes
394 -----
395 If this handler is enabled and `lsst.log` has been configured to use
396 Python `logging`, the handler will do nothing itself if any other
397 handler has been registered with the Python logger. If it does not
398 think that anything else is handling the message it will attempt to
399 send the message via a default `~logging.StreamHandler`. The safest
400 approach is to configure the logger with an additional handler
401 (possibly the ROOT logger) if `lsst.log` is to be configured to use
402 Python logging.
403 """
405 def __init__(self, level=logging.NOTSET):
406 logging.Handler.__init__(self, level=level)
407 # Format as a simple message because lsst.log will format the
408 # message a second time.
409 self.formatter = logging.Formatter(fmt="%(message)s")
411 def handle(self, record):
412 logger = Log.getLogger(record.name)
413 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
414 logging.Handler.handle(self, record)
416 def emit(self, record):
417 if Log.UsePythonLogging:
418 # Do not forward this message to lsst.log since this may cause
419 # a logging loop.
421 # Work out whether any other handler is going to be invoked
422 # for this logger.
423 pylgr = logging.getLogger(record.name)
425 # If another handler is registered that is not LogHandler
426 # we ignore this request
427 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
428 return
430 # If the parent has handlers and propagation is enabled
431 # we punt as well (and if a LogHandler is involved then we will
432 # ask the same question when we get to it).
433 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
434 return
436 # Force this message to appear somewhere.
437 # If something else should happen then the caller should add a
438 # second Handler.
439 stream = logging.StreamHandler()
440 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
441 stream.handle(record)
442 return
444 logger = Log.getLogger(record.name)
445 # Use standard formatting class to format message part of the record
446 message = self.formatter.format(record)
448 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
449 record.filename, record.funcName,
450 record.lineno, message)