Coverage for python/lsst/utils/logging.py: 53%
124 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-27 02:19 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-27 02:19 -0700
1# This file is part of utils.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = (
15 "TRACE",
16 "VERBOSE",
17 "getLogger",
18 "getTraceLogger",
19 "LsstLogAdapter",
20 "PeriodicLogger",
21 "trace_set_at",
22)
24import logging
25import time
26from contextlib import contextmanager
27from logging import LoggerAdapter
28from typing import Any, Generator, List, Optional, Union
30from deprecated.sphinx import deprecated
32try:
33 import lsst.log.utils as logUtils
34except ImportError:
35 logUtils = None
37# log level for trace (verbose debug).
38TRACE = 5
39logging.addLevelName(TRACE, "TRACE")
41# Verbose logging is midway between INFO and DEBUG.
42VERBOSE = (logging.INFO + logging.DEBUG) // 2
43logging.addLevelName(VERBOSE, "VERBOSE")
46def trace_set_at(name: str, number: int) -> None:
47 """Adjust logging level to display messages with the trace number being
48 less than or equal to the provided value.
50 Parameters
51 ----------
52 name : `str`
53 Name of the logger.
54 number : `int`
55 The trace number threshold for display.
57 Examples
58 --------
59 .. code-block:: python
61 lsst.utils.logging.trace_set_at("lsst.afw", 3)
63 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to
64 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``.
66 Notes
67 -----
68 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
69 the specified threshold are set to ``INFO`` and those below the threshold
70 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
71 issue ``DEBUG`` log messages.
73 If ``lsst.log`` is installed, this function will also call
74 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are
75 also configured correctly.
76 """
77 for i in range(6):
78 level = logging.INFO if i > number else logging.DEBUG
79 getTraceLogger(name, i).setLevel(level)
81 # if lsst log is available also set the trace loggers there.
82 if logUtils is not None:
83 logUtils.traceSetAt(name, number)
86class _F:
87 """Format, supporting `str.format()` syntax.
89 Notes
90 -----
91 This follows the recommendation from
92 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
93 """
95 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
96 self.fmt = fmt
97 self.args = args
98 self.kwargs = kwargs
100 def __str__(self) -> str:
101 return self.fmt.format(*self.args, **self.kwargs)
104class LsstLogAdapter(LoggerAdapter):
105 """A special logging adapter to provide log features for LSST code.
107 Expected to be instantiated initially by a call to `getLogger()`.
109 This class provides enhancements over `logging.Logger` that include:
111 * Methods for issuing trace and verbose level log messages.
112 * Provision of a context manager to temporarily change the log level.
113 * Attachment of logging level constants to the class to make it easier
114 for a Task writer to access a specific log level without having to
115 know the underlying logger class.
116 """
118 # Store logging constants in the class for convenience. This is not
119 # something supported by Python loggers but can simplify some
120 # logic if the logger is available.
121 CRITICAL = logging.CRITICAL
122 ERROR = logging.ERROR
123 DEBUG = logging.DEBUG
124 INFO = logging.INFO
125 WARNING = logging.WARNING
127 # Python supports these but prefers they are not used.
128 FATAL = logging.FATAL
129 WARN = logging.WARN
131 # These are specific to Tasks
132 TRACE = TRACE
133 VERBOSE = VERBOSE
135 @contextmanager
136 def temporary_log_level(self, level: Union[int, str]) -> Generator:
137 """Temporarily set the level of this logger.
139 Parameters
140 ----------
141 level : `int`
142 The new temporary log level.
143 """
144 old = self.level
145 self.setLevel(level)
146 try:
147 yield
148 finally:
149 self.setLevel(old)
151 @property
152 def level(self) -> int:
153 """Return current level of this logger (``int``)."""
154 return self.logger.level
156 def getChild(self, name: str) -> LsstLogAdapter:
157 """Get the named child logger.
159 Parameters
160 ----------
161 name : `str`
162 Name of the child relative to this logger.
164 Returns
165 -------
166 child : `LsstLogAdapter`
167 The child logger.
168 """
169 return getLogger(name=name, logger=self.logger)
171 @deprecated(
172 reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
173 version="v23",
174 category=FutureWarning,
175 )
176 def isDebugEnabled(self) -> bool:
177 return self.isEnabledFor(self.DEBUG)
179 @deprecated(
180 reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
181 version="v23",
182 category=FutureWarning,
183 )
184 def getName(self) -> str:
185 return self.name
187 @deprecated(
188 reason="Use Python Logger compatible .level property. Will be removed after v23.",
189 version="v23",
190 category=FutureWarning,
191 )
192 def getLevel(self) -> int:
193 return self.logger.level
195 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
196 # Python does not provide this method in LoggerAdapter but does
197 # not formally deprecated it in favor of critical() either.
198 # Provide it without deprecation message for consistency with Python.
199 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
200 self.critical(msg, *args, **kwargs, stacklevel=4)
202 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
203 """Issue a VERBOSE level log message.
205 Arguments are as for `logging.info`.
206 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
207 """
208 # There is no other way to achieve this other than a special logger
209 # method.
210 # Stacklevel is passed in so that the correct line is reported
211 # in the log record and not this line. 3 is this method,
212 # 2 is the level from `self.log` and 1 is the log infrastructure
213 # itself.
214 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
216 def trace(self, fmt: str, *args: Any) -> None:
217 """Issue a TRACE level log message.
219 Arguments are as for `logging.info`.
220 ``TRACE`` is lower than ``DEBUG``.
221 """
222 # There is no other way to achieve this other than a special logger
223 # method. For stacklevel discussion see `verbose()`.
224 self.log(TRACE, fmt, *args, stacklevel=3)
226 @deprecated(
227 reason="Use Python Logger compatible method. Will be removed after v23.",
228 version="v23",
229 category=FutureWarning,
230 )
231 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
232 # Stacklevel is 4 to account for the deprecation wrapper
233 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
235 @deprecated(
236 reason="Use Python Logger compatible method. Will be removed after v23.",
237 version="v23",
238 category=FutureWarning,
239 )
240 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
241 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
243 @deprecated(
244 reason="Use Python Logger compatible method. Will be removed after v23.",
245 version="v23",
246 category=FutureWarning,
247 )
248 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
249 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
251 @deprecated(
252 reason="Use Python Logger compatible method. Will be removed after v23.",
253 version="v23",
254 category=FutureWarning,
255 )
256 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
257 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
259 @deprecated(
260 reason="Use Python Logger compatible method. Will be removed after v23.",
261 version="v23",
262 category=FutureWarning,
263 )
264 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
265 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
267 @deprecated(
268 reason="Use Python Logger compatible method. Will be removed after v23.",
269 version="v23",
270 category=FutureWarning,
271 )
272 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
273 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
275 def setLevel(self, level: Union[int, str]) -> None:
276 """Set the level for the logger, trapping lsst.log values.
278 Parameters
279 ----------
280 level : `int`
281 The level to use. If the level looks too big to be a Python
282 logging level it is assumed to be a lsst.log level.
283 """
284 if isinstance(level, int) and level > logging.CRITICAL:
285 self.logger.warning(
286 "Attempting to set level to %d -- looks like an lsst.log level so scaling it accordingly.",
287 level,
288 )
289 level //= 1000
291 self.logger.setLevel(level)
293 @property
294 def handlers(self) -> List[logging.Handler]:
295 """Log handlers associated with this logger."""
296 return self.logger.handlers
298 def addHandler(self, handler: logging.Handler) -> None:
299 """Add a handler to this logger.
301 The handler is forwarded to the underlying logger.
302 """
303 self.logger.addHandler(handler)
305 def removeHandler(self, handler: logging.Handler) -> None:
306 """Remove the given handler from the underlying logger."""
307 self.logger.removeHandler(handler)
310def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
311 """Get a logger compatible with LSST usage.
313 Parameters
314 ----------
315 name : `str`, optional
316 Name of the logger. Root logger if `None`.
317 logger : `logging.Logger` or `LsstLogAdapter`
318 If given the logger is converted to the relevant logger class.
319 If ``name`` is given the logger is assumed to be a child of the
320 supplied logger.
322 Returns
323 -------
324 logger : `LsstLogAdapter`
325 The relevant logger.
327 Notes
328 -----
329 A `logging.LoggerAdapter` is used since it is easier to provide a more
330 uniform interface than when using `logging.setLoggerClass`. An adapter
331 can be wrapped around the root logger and the `~logging.setLoggerClass`
332 will return the logger first given that name even if the name was
333 used before the `Task` was created.
334 """
335 if not logger:
336 logger = logging.getLogger(name)
337 elif name:
338 logger = logger.getChild(name)
339 return LsstLogAdapter(logger, {})
342LsstLoggers = Union[logging.Logger, LsstLogAdapter]
345def getTraceLogger(logger: Union[str, LsstLoggers], trace_level: int) -> LsstLogAdapter:
346 """Get a logger with the appropriate TRACE name.
348 Parameters
349 ----------
350 logger : `logging.Logger` or `LsstLogAdapter` or `lsst.log.Log` or `str`
351 A logger to be used to derive the new trace logger. Can be a logger
352 object that supports the ``name`` property or a string.
353 trace_level : `int`
354 The trace level to use for the logger.
356 Returns
357 -------
358 trace_logger : `LsstLogAdapter`
359 A new trace logger. The name will be derived by pre-pending ``TRACEn.``
360 to the name of the supplied logger. If the root logger is given
361 the returned logger will be named ``TRACEn``.
362 """
363 name = getattr(logger, "name", str(logger))
364 trace_name = f"TRACE{trace_level}.{name}" if name else f"TRACE{trace_level}"
365 return getLogger(trace_name)
368class PeriodicLogger:
369 """Issue log messages if a time threshold has elapsed.
371 This class can be used in long-running sections of code where it would
372 be useful to issue a log message periodically to show that the
373 algorithm is progressing.
375 Parameters
376 ----------
377 logger : `logging.Logger` or `LsstLogAdapter`
378 Logger to use when issuing a message.
379 interval : `float`
380 The minimum interval between log messages. If `None` the class
381 default will be used.
382 level : `int`, optional
383 Log level to use when issuing messages.
384 """
386 LOGGING_INTERVAL = 600.0
387 """Default interval between log messages."""
389 def __init__(self, logger: LsstLoggers, interval: Optional[float] = None, level: int = VERBOSE):
390 self.logger = logger
391 self.interval = interval if interval is not None else self.LOGGING_INTERVAL
392 self.level = level
393 self.next_log_time = time.time() + self.interval
394 self.num_issued = 0
396 # The stacklevel we need to issue logs is determined by the type
397 # of logger we have been given. A LoggerAdapter has an extra
398 # level of indirection.
399 self._stacklevel = 3 if isinstance(self.logger, LoggerAdapter) else 2
401 def log(self, msg: str, *args: Any) -> bool:
402 """Issue a log message if the interval has elapsed.
404 Parameters
405 ----------
406 msg : `str`
407 Message to issue if the time has been exceeded.
408 *args : Any
409 Parameters to be passed to the log system.
411 Returns
412 -------
413 issued : `bool`
414 Returns `True` if a log message was sent to the logging system.
415 Returns `False` if the interval has not yet elapsed. Returning
416 `True` does not indicate whether the log message was in fact
417 issued by the logging system.
418 """
419 if (current_time := time.time()) > self.next_log_time:
420 self.logger.log(self.level, msg, *args, stacklevel=self._stacklevel)
421 self.next_log_time = current_time + self.interval
422 self.num_issued += 1
423 return True
424 return False