LSST Applications g0265f82a02+0e5473021a,g02d81e74bb+f5613e8b4f,g1470d8bcf6+190ad2ba91,g14a832a312+311607e4ab,g2079a07aa2+86d27d4dc4,g2305ad1205+a8e3196225,g295015adf3+b67ee847e5,g2bbee38e9b+0e5473021a,g337abbeb29+0e5473021a,g3ddfee87b4+a761f810f3,g487adcacf7+17c8fdbcbd,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+65b5bd823e,g5a732f18d5+53520f316c,g64a986408d+f5613e8b4f,g6c1bc301e9+51106c2951,g858d7b2824+f5613e8b4f,g8a8a8dda67+585e252eca,g99cad8db69+6729933424,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,ga8c6da7877+ef4e3a5875,gb0e22166c9+60f28cb32d,gb6a65358fc+0e5473021a,gba4ed39666+c2a2e4ac27,gbb8dafda3b+e9bba80f27,gc120e1dc64+eee469a5e5,gc28159a63d+0e5473021a,gcf0d15dbbd+a761f810f3,gdaeeff99f8+f9a426f77a,ge6526c86ff+d4c1d4bfef,ge79ae78c31+0e5473021a,gee10cc3b42+585e252eca,gf1cff7945b+f5613e8b4f,w.2024.16
LSST Data Management Base Package
Loading...
Searching...
No Matches
logContinued.py
Go to the documentation of this file.
1#!/usr/bin/env python
2
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#
24
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",
30 "lwpID", "usePythonLogging", "doNotUsePythonLogging", "UsePythonLogging",
31 "LevelTranslator", "LogHandler", "getEffectiveLevel", "getLevelName"]
32
33import logging
34import inspect
35import os
36
37from typing import Optional
38
39from lsst.utils import continueClass
40
41from .log import Log
42
43TRACE = 5000
44DEBUG = 10000
45INFO = 20000
46WARN = 30000
47ERROR = 40000
48FATAL = 50000
49
50# For compatibility with python logging
51CRITICAL = FATAL
52WARNING = WARN
53
54
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."""
59
60 CRITICAL = CRITICAL
61 WARNING = WARNING
62
63 @classmethod
65 """Forward log messages to Python `logging`
66
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.
72
73 This state only affects messages sent to the `lsst.log`
74 package from Python.
75 """
77
78 @classmethod
80 """Forward log messages to LSST logging system.
81
82 Notes
83 -----
84 This is the default state.
85 """
87
88 @property
89 def name(self):
90 return self.getName()
91
92 @property
93 def level(self):
94 return self.getLevel()
95
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)
105
106 def trace(self, fmt, *args):
107 self._log(Log.TRACE, False, fmt, *args)
108
109 def debug(self, fmt, *args):
110 self._log(Log.DEBUG, False, fmt, *args)
111
112 def info(self, fmt, *args):
113 self._log(Log.INFO, False, fmt, *args)
114
115 def warn(self, fmt, *args):
116 self._log(Log.WARN, False, fmt, *args)
117
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)
122
123 def error(self, fmt, *args):
124 self._log(Log.ERROR, False, fmt, *args)
125
126 def fatal(self, fmt, *args):
127 self._log(Log.FATAL, False, fmt, *args)
128
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)
133
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
145 levelno = LevelTranslator.lsstLog2logging(level)
146 levelName = logging.getLevelName(levelno)
147
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)
160
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)
167
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())})>"
176
177
178class MDCDict(dict):
179 """Dictionary for MDC data.
180
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, "")
189
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) + "}"
196
197 def __repr__(self):
198 return str(self)
199
200
201# Export static functions from Log class to module namespace
202
203
204def configure(*args):
205 Log.configure(*args)
206
207
208def configure_prop(properties):
209 Log.configure_prop(properties)
210
211
212def configure_pylog_MDC(level: str, MDC_class: Optional[type] = MDCDict):
213 """Configure log4cxx to send messages to Python logging, with MDC support.
214
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.
223
224 Notes
225 -----
226 This method does two things:
227
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()
237
238 def record_factory(*args, **kwargs):
239 record = old_factory(*args, **kwargs)
240 record.MDC = MDC_class()
241 return record
242
243 logging.setLogRecordFactory(record_factory)
244
245 properties = """\
246log4j.rootLogger = {}, PyLog
247log4j.appender.PyLog = PyLogAppender
248""".format(level)
249 configure_prop(properties)
250
251
253 return Log.getDefaultLogger()
254
255
256def getLogger(loggername):
257 return Log.getLogger(loggername)
258
259
260def MDC(key, value):
261 return Log.MDC(key, str(value))
262
263
264def MDCRemove(key):
265 Log.MDCRemove(key)
266
267
269 Log.MDCRegisterInit(func)
270
271
272def setLevel(loggername, level):
273 Log.getLogger(loggername).setLevel(level)
274
275
276def getLevel(loggername):
277 return Log.getLogger(loggername).getLevel()
278
279
280def getEffectiveLevel(loggername):
281 return Log.getLogger(loggername).getEffectiveLevel()
282
283
284def isEnabledFor(loggername, level):
285 return Log.getLogger(loggername).isEnabledFor(level)
286
287
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)
293
294
295def trace(fmt, *args):
296 Log.getDefaultLogger()._log(TRACE, False, fmt, *args)
297
298
299def debug(fmt, *args):
300 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args)
301
302
303def info(fmt, *args):
304 Log.getDefaultLogger()._log(INFO, False, fmt, *args)
305
306
307def warn(fmt, *args):
308 Log.getDefaultLogger()._log(WARN, False, fmt, *args)
309
310
311def warning(fmt, *args):
312 warn(fmt, *args)
313
314
315def error(fmt, *args):
316 Log.getDefaultLogger()._log(ERROR, False, fmt, *args)
317
318
319def fatal(fmt, *args):
320 Log.getDefaultLogger()._log(FATAL, False, fmt, *args)
321
322
323def critical(fmt, *args):
324 fatal(fmt, *args)
325
326
327def lwpID():
328 return Log.lwpID
329
330
331def getLevelName(level):
332 """Return the name associated with this logging level.
333
334 Returns "Level %d" if no name can be found.
335 """
336 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR")
337 for name in names:
338 test_level = getattr(Log, name)
339 if test_level == level:
340 return name
341 return f"Level {level}"
342
343
344# This will cause a warning in Sphinx documentation due to confusion between
345# UsePythonLogging and usePythonLogging.
346# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this
347# does not seem to be Mac-only).
349 Log.usePythonLogging()
350
351
353 Log.doNotUsePythonLogging()
354
355
357 """Context manager to enable Python log forwarding temporarily.
358 """
359
360 def __init__(self):
361 self.current = Log.UsePythonLogging
362
363 def __enter__(self):
364 Log.usePythonLogging()
365
366 def __exit__(self, exc_type, exc_value, traceback):
367 Log.UsePythonLogging = self.current
368
369
371 """Helper class to translate levels between ``lsst.log`` and Python
372 `logging`.
373 """
374 @staticmethod
375 def lsstLog2logging(level):
376 """Translates from lsst.log/log4cxx levels to `logging` module levels.
377
378 Parameters
379 ----------
380 level : `int`
381 Logging level number used by `lsst.log`, typically one of the
382 constants defined in this module (`DEBUG`, `INFO`, etc.)
383
384 Returns
385 -------
386 level : `int`
387 Correspoding logging level number for Python `logging` module.
388 """
389 # Python logging levels are same as lsst.log divided by 1000,
390 # logging does not have TRACE level by default but it is OK to use
391 # that numeric level and we may even add TRACE later.
392 return level//1000
393
394 @staticmethod
395 def logging2lsstLog(level):
396 """Translates from standard python `logging` module levels to
397 lsst.log/log4cxx levels.
398
399 Parameters
400 ----------
401 level : `int`
402 Logging level number used by Python `logging`, typically one of
403 the constants defined by `logging` module (`logging.DEBUG`,
404 `logging.INFO`, etc.)
405
406 Returns
407 -------
408 level : `int`
409 Correspoding logging level number for `lsst.log` module.
410 """
411 return level*1000
412
413
414class LogHandler(logging.Handler):
415 """Handler for Python logging module that emits to LSST logging.
416
417 Parameters
418 ----------
419 level : `int`
420 Level at which to set the this handler.
421
422 Notes
423 -----
424 If this handler is enabled and `lsst.log` has been configured to use
425 Python `logging`, the handler will do nothing itself if any other
426 handler has been registered with the Python logger. If it does not
427 think that anything else is handling the message it will attempt to
428 send the message via a default `~logging.StreamHandler`. The safest
429 approach is to configure the logger with an additional handler
430 (possibly the ROOT logger) if `lsst.log` is to be configured to use
431 Python logging.
432 """
433
434 def __init__(self, level=logging.NOTSET):
435 logging.Handler.__init__(self, level=level)
436 # Format as a simple message because lsst.log will format the
437 # message a second time.
438 self.formatter = logging.Formatter(fmt="%(message)s")
439
440 def handle(self, record):
441 logger = Log.getLogger(record.name)
442 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)):
443 logging.Handler.handle(self, record)
444
445 def emit(self, record):
446 if Log.UsePythonLogging:
447 # Do not forward this message to lsst.log since this may cause
448 # a logging loop.
449
450 # Work out whether any other handler is going to be invoked
451 # for this logger.
452 pylgr = logging.getLogger(record.name)
453
454 # If another handler is registered that is not LogHandler
455 # we ignore this request
456 if any(not isinstance(h, self.__class__) for h in pylgr.handlers):
457 return
458
459 # If the parent has handlers and propagation is enabled
460 # we punt as well (and if a LogHandler is involved then we will
461 # ask the same question when we get to it).
462 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate:
463 return
464
465 # Force this message to appear somewhere.
466 # If something else should happen then the caller should add a
467 # second Handler.
468 stream = logging.StreamHandler()
469 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s"))
470 stream.handle(record)
471 return
472
473 logger = Log.getLogger(record.name)
474 # Use standard formatting class to format message part of the record
475 message = self.formatter.format(record)
476
477 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno),
478 record.filename, record.funcName,
479 record.lineno, message)
table::Key< std::string > name
Definition Amplifier.cc:116
__init__(self, level=logging.NOTSET)
_log(self, level, use_format, fmt, *args, **kwargs)
__exit__(self, exc_type, exc_value, traceback)
configure_pylog_MDC(str level, Optional[type] MDC_class=MDCDict)
isEnabledFor(loggername, level)
setLevel(loggername, level)