Coverage for python/lsst/utils/logging.py: 46%
96 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-14 01:59 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-14 01:59 -0800
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
30try:
31 import lsst.log.utils as logUtils
32except ImportError:
33 logUtils = None
35# log level for trace (verbose debug).
36TRACE = 5
37logging.addLevelName(TRACE, "TRACE")
39# Verbose logging is midway between INFO and DEBUG.
40VERBOSE = (logging.INFO + logging.DEBUG) // 2
41logging.addLevelName(VERBOSE, "VERBOSE")
44def trace_set_at(name: str, number: int) -> None:
45 """Adjust logging level to display messages with the trace number being
46 less than or equal to the provided value.
48 Parameters
49 ----------
50 name : `str`
51 Name of the logger.
52 number : `int`
53 The trace number threshold for display.
55 Examples
56 --------
57 .. code-block:: python
59 lsst.utils.logging.trace_set_at("lsst.afw", 3)
61 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to
62 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``.
64 Notes
65 -----
66 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
67 the specified threshold are set to ``INFO`` and those below the threshold
68 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
69 issue ``DEBUG`` log messages.
71 If ``lsst.log`` is installed, this function will also call
72 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are
73 also configured correctly.
74 """
75 for i in range(6):
76 level = logging.INFO if i > number else logging.DEBUG
77 getTraceLogger(name, i).setLevel(level)
79 # if lsst log is available also set the trace loggers there.
80 if logUtils is not None:
81 logUtils.traceSetAt(name, number)
84class _F:
85 """Format, supporting `str.format()` syntax.
87 Notes
88 -----
89 This follows the recommendation from
90 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
91 """
93 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
94 self.fmt = fmt
95 self.args = args
96 self.kwargs = kwargs
98 def __str__(self) -> str:
99 return self.fmt.format(*self.args, **self.kwargs)
102class LsstLogAdapter(LoggerAdapter):
103 """A special logging adapter to provide log features for LSST code.
105 Expected to be instantiated initially by a call to `getLogger()`.
107 This class provides enhancements over `logging.Logger` that include:
109 * Methods for issuing trace and verbose level log messages.
110 * Provision of a context manager to temporarily change the log level.
111 * Attachment of logging level constants to the class to make it easier
112 for a Task writer to access a specific log level without having to
113 know the underlying logger class.
114 """
116 # Store logging constants in the class for convenience. This is not
117 # something supported by Python loggers but can simplify some
118 # logic if the logger is available.
119 CRITICAL = logging.CRITICAL
120 ERROR = logging.ERROR
121 DEBUG = logging.DEBUG
122 INFO = logging.INFO
123 WARNING = logging.WARNING
125 # Python supports these but prefers they are not used.
126 FATAL = logging.FATAL
127 WARN = logging.WARN
129 # These are specific to Tasks
130 TRACE = TRACE
131 VERBOSE = VERBOSE
133 @contextmanager
134 def temporary_log_level(self, level: Union[int, str]) -> Generator:
135 """Temporarily set the level of this logger.
137 Parameters
138 ----------
139 level : `int`
140 The new temporary log level.
141 """
142 old = self.level
143 self.setLevel(level)
144 try:
145 yield
146 finally:
147 self.setLevel(old)
149 @property
150 def level(self) -> int:
151 """Return current level of this logger (``int``)."""
152 return self.logger.level
154 def getChild(self, name: str) -> LsstLogAdapter:
155 """Get the named child logger.
157 Parameters
158 ----------
159 name : `str`
160 Name of the child relative to this logger.
162 Returns
163 -------
164 child : `LsstLogAdapter`
165 The child logger.
166 """
167 return getLogger(name=name, logger=self.logger)
169 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
170 # Python does not provide this method in LoggerAdapter but does
171 # not formally deprecated it in favor of critical() either.
172 # Provide it without deprecation message for consistency with Python.
173 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
174 self.critical(msg, *args, **kwargs, stacklevel=4)
176 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
177 """Issue a VERBOSE level log message.
179 Arguments are as for `logging.info`.
180 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
181 """
182 # There is no other way to achieve this other than a special logger
183 # method.
184 # Stacklevel is passed in so that the correct line is reported
185 # in the log record and not this line. 3 is this method,
186 # 2 is the level from `self.log` and 1 is the log infrastructure
187 # itself.
188 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
190 def trace(self, fmt: str, *args: Any) -> None:
191 """Issue a TRACE level log message.
193 Arguments are as for `logging.info`.
194 ``TRACE`` is lower than ``DEBUG``.
195 """
196 # There is no other way to achieve this other than a special logger
197 # method. For stacklevel discussion see `verbose()`.
198 self.log(TRACE, fmt, *args, stacklevel=3)
200 def setLevel(self, level: Union[int, str]) -> None:
201 """Set the level for the logger, trapping lsst.log values.
203 Parameters
204 ----------
205 level : `int`
206 The level to use. If the level looks too big to be a Python
207 logging level it is assumed to be a lsst.log level.
208 """
209 if isinstance(level, int) and level > logging.CRITICAL:
210 self.logger.warning(
211 "Attempting to set level to %d -- looks like an lsst.log level so scaling it accordingly.",
212 level,
213 )
214 level //= 1000
216 self.logger.setLevel(level)
218 @property
219 def handlers(self) -> List[logging.Handler]:
220 """Log handlers associated with this logger."""
221 return self.logger.handlers
223 def addHandler(self, handler: logging.Handler) -> None:
224 """Add a handler to this logger.
226 The handler is forwarded to the underlying logger.
227 """
228 self.logger.addHandler(handler)
230 def removeHandler(self, handler: logging.Handler) -> None:
231 """Remove the given handler from the underlying logger."""
232 self.logger.removeHandler(handler)
235def getLogger(name: Optional[str] = None, logger: Optional[logging.Logger] = None) -> LsstLogAdapter:
236 """Get a logger compatible with LSST usage.
238 Parameters
239 ----------
240 name : `str`, optional
241 Name of the logger. Root logger if `None`.
242 logger : `logging.Logger` or `LsstLogAdapter`
243 If given the logger is converted to the relevant logger class.
244 If ``name`` is given the logger is assumed to be a child of the
245 supplied logger.
247 Returns
248 -------
249 logger : `LsstLogAdapter`
250 The relevant logger.
252 Notes
253 -----
254 A `logging.LoggerAdapter` is used since it is easier to provide a more
255 uniform interface than when using `logging.setLoggerClass`. An adapter
256 can be wrapped around the root logger and the `~logging.setLoggerClass`
257 will return the logger first given that name even if the name was
258 used before the `Task` was created.
259 """
260 if not logger:
261 logger = logging.getLogger(name)
262 elif name:
263 logger = logger.getChild(name)
264 return LsstLogAdapter(logger, {})
267LsstLoggers = Union[logging.Logger, LsstLogAdapter]
270def getTraceLogger(logger: Union[str, LsstLoggers], trace_level: int) -> LsstLogAdapter:
271 """Get a logger with the appropriate TRACE name.
273 Parameters
274 ----------
275 logger : `logging.Logger` or `LsstLogAdapter` or `lsst.log.Log` or `str`
276 A logger to be used to derive the new trace logger. Can be a logger
277 object that supports the ``name`` property or a string.
278 trace_level : `int`
279 The trace level to use for the logger.
281 Returns
282 -------
283 trace_logger : `LsstLogAdapter`
284 A new trace logger. The name will be derived by pre-pending ``TRACEn.``
285 to the name of the supplied logger. If the root logger is given
286 the returned logger will be named ``TRACEn``.
287 """
288 name = getattr(logger, "name", str(logger))
289 trace_name = f"TRACE{trace_level}.{name}" if name else f"TRACE{trace_level}"
290 return getLogger(trace_name)
293class PeriodicLogger:
294 """Issue log messages if a time threshold has elapsed.
296 This class can be used in long-running sections of code where it would
297 be useful to issue a log message periodically to show that the
298 algorithm is progressing.
300 Parameters
301 ----------
302 logger : `logging.Logger` or `LsstLogAdapter`
303 Logger to use when issuing a message.
304 interval : `float`
305 The minimum interval between log messages. If `None` the class
306 default will be used.
307 level : `int`, optional
308 Log level to use when issuing messages.
309 """
311 LOGGING_INTERVAL = 600.0
312 """Default interval between log messages."""
314 def __init__(self, logger: LsstLoggers, interval: Optional[float] = None, level: int = VERBOSE):
315 self.logger = logger
316 self.interval = interval if interval is not None else self.LOGGING_INTERVAL
317 self.level = level
318 self.next_log_time = time.time() + self.interval
319 self.num_issued = 0
321 # The stacklevel we need to issue logs is determined by the type
322 # of logger we have been given. A LoggerAdapter has an extra
323 # level of indirection.
324 self._stacklevel = 3 if isinstance(self.logger, LoggerAdapter) else 2
326 def log(self, msg: str, *args: Any) -> bool:
327 """Issue a log message if the interval has elapsed.
329 Parameters
330 ----------
331 msg : `str`
332 Message to issue if the time has been exceeded.
333 *args : Any
334 Parameters to be passed to the log system.
336 Returns
337 -------
338 issued : `bool`
339 Returns `True` if a log message was sent to the logging system.
340 Returns `False` if the interval has not yet elapsed. Returning
341 `True` does not indicate whether the log message was in fact
342 issued by the logging system.
343 """
344 if (current_time := time.time()) > self.next_log_time:
345 self.logger.log(self.level, msg, *args, stacklevel=self._stacklevel)
346 self.next_log_time = current_time + self.interval
347 self.num_issued += 1
348 return True
349 return False