Coverage for python/lsst/utils/logging.py: 53%
Shortcuts 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
Shortcuts 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# 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__ = ("TRACE", "VERBOSE", "getLogger", "LsstLogAdapter", "PeriodicLogger", "trace_set_at")
16import logging
17import time
18from contextlib import contextmanager
19from logging import LoggerAdapter
20from typing import Any, Generator, List, Optional, Union
22from deprecated.sphinx import deprecated
24try:
25 import lsst.log.utils as logUtils
26except ImportError:
27 logUtils = None
29# log level for trace (verbose debug).
30TRACE = 5
31logging.addLevelName(TRACE, "TRACE")
33# Verbose logging is midway between INFO and DEBUG.
34VERBOSE = (logging.INFO + logging.DEBUG) // 2
35logging.addLevelName(VERBOSE, "VERBOSE")
38def trace_set_at(name: str, number: int) -> None:
39 """Adjust logging level to display messages with the trace number being
40 less than or equal to the provided value.
42 Parameters
43 ----------
44 name : `str`
45 Name of the logger.
46 number : `int`
47 The trace number threshold for display.
49 Examples
50 --------
51 .. code-block:: python
53 lsst.utils.logging.trace_set_at("lsst.afw", 3)
55 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to
56 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``.
58 Notes
59 -----
60 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
61 the specified threshold are set to ``INFO`` and those below the threshold
62 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
63 issue ``DEBUG`` log messages.
65 If ``lsst.log`` is installed, this function will also call
66 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are
67 also configured correctly.
68 """
69 for i in range(6):
70 level = logging.INFO if i > number else logging.DEBUG
71 log_name = f"TRACE{i}.{name}" if name else f"TRACE{i}"
72 logging.getLogger(log_name).setLevel(level)
74 # if lsst log is available also set the trace loggers there.
75 if logUtils is not None:
76 logUtils.traceSetAt(name, number)
79class _F:
80 """Format, supporting `str.format()` syntax.
82 Notes
83 -----
84 This follows the recommendation from
85 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
86 """
88 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
89 self.fmt = fmt
90 self.args = args
91 self.kwargs = kwargs
93 def __str__(self) -> str:
94 return self.fmt.format(*self.args, **self.kwargs)
97class LsstLogAdapter(LoggerAdapter):
98 """A special logging adapter to provide log features for LSST code.
100 Expected to be instantiated initially by a call to `getLogger()`.
102 This class provides enhancements over `logging.Logger` that include:
104 * Methods for issuing trace and verbose level log messages.
105 * Provision of a context manager to temporarily change the log level.
106 * Attachment of logging level constants to the class to make it easier
107 for a Task writer to access a specific log level without having to
108 know the underlying logger class.
109 """
111 # Store logging constants in the class for convenience. This is not
112 # something supported by Python loggers but can simplify some
113 # logic if the logger is available.
114 CRITICAL = logging.CRITICAL
115 ERROR = logging.ERROR
116 DEBUG = logging.DEBUG
117 INFO = logging.INFO
118 WARNING = logging.WARNING
120 # Python supports these but prefers they are not used.
121 FATAL = logging.FATAL
122 WARN = logging.WARN
124 # These are specific to Tasks
125 TRACE = TRACE
126 VERBOSE = VERBOSE
128 @contextmanager
129 def temporary_log_level(self, level: Union[int, str]) -> Generator:
130 """Temporarily set the level of this logger.
132 Parameters
133 ----------
134 level : `int`
135 The new temporary log level.
136 """
137 old = self.level
138 self.setLevel(level)
139 try:
140 yield
141 finally:
142 self.setLevel(old)
144 @property
145 def level(self) -> int:
146 """Return current level of this logger (``int``)."""
147 return self.logger.level
149 def getChild(self, name: str) -> LsstLogAdapter:
150 """Get the named child logger.
152 Parameters
153 ----------
154 name : `str`
155 Name of the child relative to this logger.
157 Returns
158 -------
159 child : `LsstLogAdapter`
160 The child logger.
161 """
162 return getLogger(name=name, logger=self.logger)
164 @deprecated(
165 reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
166 version="v23",
167 category=FutureWarning,
168 )
169 def isDebugEnabled(self) -> bool:
170 return self.isEnabledFor(self.DEBUG)
172 @deprecated(
173 reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
174 version="v23",
175 category=FutureWarning,
176 )
177 def getName(self) -> str:
178 return self.name
180 @deprecated(
181 reason="Use Python Logger compatible .level property. Will be removed after v23.",
182 version="v23",
183 category=FutureWarning,
184 )
185 def getLevel(self) -> int:
186 return self.logger.level
188 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
189 # Python does not provide this method in LoggerAdapter but does
190 # not formally deprecated it in favor of critical() either.
191 # Provide it without deprecation message for consistency with Python.
192 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
193 self.critical(msg, *args, **kwargs, stacklevel=4)
195 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
196 """Issue a VERBOSE level log message.
198 Arguments are as for `logging.info`.
199 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
200 """
201 # There is no other way to achieve this other than a special logger
202 # method.
203 # Stacklevel is passed in so that the correct line is reported
204 # in the log record and not this line. 3 is this method,
205 # 2 is the level from `self.log` and 1 is the log infrastructure
206 # itself.
207 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
209 def trace(self, fmt: str, *args: Any) -> None:
210 """Issue a TRACE level log message.
212 Arguments are as for `logging.info`.
213 ``TRACE`` is lower than ``DEBUG``.
214 """
215 # There is no other way to achieve this other than a special logger
216 # method. For stacklevel discussion see `verbose()`.
217 self.log(TRACE, fmt, *args, stacklevel=3)
219 @deprecated(
220 reason="Use Python Logger compatible method. Will be removed after v23.",
221 version="v23",
222 category=FutureWarning,
223 )
224 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
225 # Stacklevel is 4 to account for the deprecation wrapper
226 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
228 @deprecated(
229 reason="Use Python Logger compatible method. Will be removed after v23.",
230 version="v23",
231 category=FutureWarning,
232 )
233 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
234 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
236 @deprecated(
237 reason="Use Python Logger compatible method. Will be removed after v23.",
238 version="v23",
239 category=FutureWarning,
240 )
241 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
242 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
244 @deprecated(
245 reason="Use Python Logger compatible method. Will be removed after v23.",
246 version="v23",
247 category=FutureWarning,
248 )
249 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
250 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
252 @deprecated(
253 reason="Use Python Logger compatible method. Will be removed after v23.",
254 version="v23",
255 category=FutureWarning,
256 )
257 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
258 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
260 @deprecated(
261 reason="Use Python Logger compatible method. Will be removed after v23.",
262 version="v23",
263 category=FutureWarning,
264 )
265 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
266 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
268 def setLevel(self, level: Union[int, str]) -> None:
269 """Set the level for the logger, trapping lsst.log values.
271 Parameters
272 ----------
273 level : `int`
274 The level to use. If the level looks too big to be a Python
275 logging level it is assumed to be a lsst.log level.
276 """
277 if isinstance(level, int) and level > logging.CRITICAL:
278 self.logger.warning(
279 "Attempting to set level to %d -- looks like an lsst.log level so scaling it accordingly.",
280 level,
281 )
282 level //= 1000
284 self.logger.setLevel(level)
286 @property
287 def handlers(self) -> List[logging.Handler]:
288 """Log handlers associated with this logger."""
289 return self.logger.handlers
291 def addHandler(self, handler: logging.Handler) -> None:
292 """Add a handler to this logger.
294 The handler is forwarded to the underlying logger.
295 """
296 self.logger.addHandler(handler)
298 def removeHandler(self, handler: logging.Handler) -> None:
299 """Remove the given handler from the underlying logger."""
300 self.logger.removeHandler(handler)
303def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
304 """Get a logger compatible with LSST usage.
306 Parameters
307 ----------
308 name : `str`, optional
309 Name of the logger. Root logger if `None`.
310 logger : `logging.Logger` or `LsstLogAdapter`
311 If given the logger is converted to the relevant logger class.
312 If ``name`` is given the logger is assumed to be a child of the
313 supplied logger.
315 Returns
316 -------
317 logger : `LsstLogAdapter`
318 The relevant logger.
320 Notes
321 -----
322 A `logging.LoggerAdapter` is used since it is easier to provide a more
323 uniform interface than when using `logging.setLoggerClass`. An adapter
324 can be wrapped around the root logger and the `~logging.setLoggerClass`
325 will return the logger first given that name even if the name was
326 used before the `Task` was created.
327 """
328 if not logger:
329 logger = logging.getLogger(name)
330 elif name:
331 logger = logger.getChild(name)
332 return LsstLogAdapter(logger, {})
335LsstLoggers = Union[logging.Logger, LsstLogAdapter]
338class PeriodicLogger:
339 """Issue log messages if a time threshold has elapsed.
341 This class can be used in long-running sections of code where it would
342 be useful to issue a log message periodically to show that the
343 algorithm is progressing.
345 Parameters
346 ----------
347 logger : `logging.Logger` or `LsstLogAdapter`
348 Logger to use when issuing a message.
349 interval : `float`
350 The minimum interval between log messages. If `None` the class
351 default will be used.
352 level : `int`, optional
353 Log level to use when issuing messages.
354 """
356 LOGGING_INTERVAL = 600.0
357 """Default interval between log messages."""
359 def __init__(self, logger: LsstLoggers, interval: Optional[float] = None, level: int = VERBOSE):
360 self.logger = logger
361 self.interval = interval if interval is not None else self.LOGGING_INTERVAL
362 self.level = level
363 self.next_log_time = time.time() + self.interval
364 self.num_issued = 0
366 def log(self, msg: str, *args: Any) -> bool:
367 """Issue a log message if the interval has elapsed.
369 Parameters
370 ----------
371 msg : `str`
372 Message to issue if the time has been exceeded.
373 *args : Any
374 Parameters to be passed to the log system.
376 Returns
377 -------
378 issued : `bool`
379 Returns `True` if a log message was sent to the logging system.
380 Returns `False` if the interval has not yet elapsed. Returning
381 `True` does not indicate whether the log message was in fact
382 issued by the logging system.
383 """
384 if (current_time := time.time()) > self.next_log_time:
385 self.logger.log(self.level, msg, *args)
386 self.next_log_time = current_time + self.interval
387 self.num_issued += 1
388 return True
389 return False