Coverage for python/lsst/utils/logging.py: 56%
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", "trace_set_at")
16import logging
17from contextlib import contextmanager
18from logging import LoggerAdapter
19from typing import Any, Generator, List, Optional, Union
21from deprecated.sphinx import deprecated
23try:
24 import lsst.log.utils as logUtils
25except ImportError:
26 logUtils = None
28# log level for trace (verbose debug).
29TRACE = 5
30logging.addLevelName(TRACE, "TRACE")
32# Verbose logging is midway between INFO and DEBUG.
33VERBOSE = (logging.INFO + logging.DEBUG) // 2
34logging.addLevelName(VERBOSE, "VERBOSE")
37def trace_set_at(name: str, number: int) -> None:
38 """Adjust logging level to display messages with the trace number being
39 less than or equal to the provided value.
41 Parameters
42 ----------
43 name : `str`
44 Name of the logger.
45 number : `int`
46 The trace number threshold for display.
48 Examples
49 --------
50 .. code-block:: python
52 lsst.utils.logging.trace_set_at("lsst.afw", 3)
54 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to
55 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``.
57 Notes
58 -----
59 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
60 the specified threshold are set to ``INFO`` and those below the threshold
61 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
62 issue ``DEBUG`` log messages.
64 If ``lsst.log`` is installed, this function will also call
65 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are
66 also configured correctly.
67 """
68 for i in range(6):
69 level = logging.INFO if i > number else logging.DEBUG
70 log_name = f"TRACE{i}.{name}" if name else f"TRACE{i}"
71 logging.getLogger(log_name).setLevel(level)
73 # if lsst log is available also set the trace loggers there.
74 if logUtils is not None:
75 logUtils.traceSetAt(name, number)
78class _F:
79 """Format, supporting `str.format()` syntax.
81 Notes
82 -----
83 This follows the recommendation from
84 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
85 """
87 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
88 self.fmt = fmt
89 self.args = args
90 self.kwargs = kwargs
92 def __str__(self) -> str:
93 return self.fmt.format(*self.args, **self.kwargs)
96class LsstLogAdapter(LoggerAdapter):
97 """A special logging adapter to provide log features for LSST code.
99 Expected to be instantiated initially by a call to `getLogger()`.
101 This class provides enhancements over `logging.Logger` that include:
103 * Methods for issuing trace and verbose level log messages.
104 * Provision of a context manager to temporarily change the log level.
105 * Attachment of logging level constants to the class to make it easier
106 for a Task writer to access a specific log level without having to
107 know the underlying logger class.
108 """
110 # Store logging constants in the class for convenience. This is not
111 # something supported by Python loggers but can simplify some
112 # logic if the logger is available.
113 CRITICAL = logging.CRITICAL
114 ERROR = logging.ERROR
115 DEBUG = logging.DEBUG
116 INFO = logging.INFO
117 WARNING = logging.WARNING
119 # Python supports these but prefers they are not used.
120 FATAL = logging.FATAL
121 WARN = logging.WARN
123 # These are specific to Tasks
124 TRACE = TRACE
125 VERBOSE = VERBOSE
127 @contextmanager
128 def temporary_log_level(self, level: Union[int, str]) -> Generator:
129 """Temporarily set the level of this logger.
131 Parameters
132 ----------
133 level : `int`
134 The new temporary log level.
135 """
136 old = self.level
137 self.setLevel(level)
138 try:
139 yield
140 finally:
141 self.setLevel(old)
143 @property
144 def level(self) -> int:
145 """Return current level of this logger (``int``)."""
146 return self.logger.level
148 def getChild(self, name: str) -> LsstLogAdapter:
149 """Get the named child logger.
151 Parameters
152 ----------
153 name : `str`
154 Name of the child relative to this logger.
156 Returns
157 -------
158 child : `LsstLogAdapter`
159 The child logger.
160 """
161 return getLogger(name=name, logger=self.logger)
163 @deprecated(
164 reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
165 version="v23",
166 category=FutureWarning,
167 )
168 def isDebugEnabled(self) -> bool:
169 return self.isEnabledFor(self.DEBUG)
171 @deprecated(
172 reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
173 version="v23",
174 category=FutureWarning,
175 )
176 def getName(self) -> str:
177 return self.name
179 @deprecated(
180 reason="Use Python Logger compatible .level property. Will be removed after v23.",
181 version="v23",
182 category=FutureWarning,
183 )
184 def getLevel(self) -> int:
185 return self.logger.level
187 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
188 # Python does not provide this method in LoggerAdapter but does
189 # not formally deprecated it in favor of critical() either.
190 # Provide it without deprecation message for consistency with Python.
191 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
192 self.critical(msg, *args, **kwargs, stacklevel=4)
194 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
195 """Issue a VERBOSE level log message.
197 Arguments are as for `logging.info`.
198 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
199 """
200 # There is no other way to achieve this other than a special logger
201 # method.
202 # Stacklevel is passed in so that the correct line is reported
203 # in the log record and not this line. 3 is this method,
204 # 2 is the level from `self.log` and 1 is the log infrastructure
205 # itself.
206 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
208 def trace(self, fmt: str, *args: Any) -> None:
209 """Issue a TRACE level log message.
211 Arguments are as for `logging.info`.
212 ``TRACE`` is lower than ``DEBUG``.
213 """
214 # There is no other way to achieve this other than a special logger
215 # method. For stacklevel discussion see `verbose()`.
216 self.log(TRACE, fmt, *args, stacklevel=3)
218 @deprecated(
219 reason="Use Python Logger compatible method. Will be removed after v23.",
220 version="v23",
221 category=FutureWarning,
222 )
223 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
224 # Stacklevel is 4 to account for the deprecation wrapper
225 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
227 @deprecated(
228 reason="Use Python Logger compatible method. Will be removed after v23.",
229 version="v23",
230 category=FutureWarning,
231 )
232 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
233 self.log(logging.DEBUG, _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 infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
241 self.log(logging.INFO, _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 warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
249 self.log(logging.WARNING, _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 errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
257 self.log(logging.ERROR, _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 fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
265 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
267 def setLevel(self, level: Union[int, str]) -> None:
268 """Set the level for the logger, trapping lsst.log values.
270 Parameters
271 ----------
272 level : `int`
273 The level to use. If the level looks too big to be a Python
274 logging level it is assumed to be a lsst.log level.
275 """
276 if isinstance(level, int) and level > logging.CRITICAL:
277 self.logger.warning(
278 "Attempting to set level to %d -- looks like an lsst.log level so scaling it accordingly.",
279 level,
280 )
281 level //= 1000
283 self.logger.setLevel(level)
285 @property
286 def handlers(self) -> List[logging.Handler]:
287 """Log handlers associated with this logger."""
288 return self.logger.handlers
290 def addHandler(self, handler: logging.Handler) -> None:
291 """Add a handler to this logger.
293 The handler is forwarded to the underlying logger.
294 """
295 self.logger.addHandler(handler)
297 def removeHandler(self, handler: logging.Handler) -> None:
298 """Remove the given handler from the underlying logger."""
299 self.logger.removeHandler(handler)
302def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
303 """Get a logger compatible with LSST usage.
305 Parameters
306 ----------
307 name : `str`, optional
308 Name of the logger. Root logger if `None`.
309 logger : `logging.Logger` or `LsstLogAdapter`
310 If given the logger is converted to the relevant logger class.
311 If ``name`` is given the logger is assumed to be a child of the
312 supplied logger.
314 Returns
315 -------
316 logger : `LsstLogAdapter`
317 The relevant logger.
319 Notes
320 -----
321 A `logging.LoggerAdapter` is used since it is easier to provide a more
322 uniform interface than when using `logging.setLoggerClass`. An adapter
323 can be wrapped around the root logger and the `~logging.setLoggerClass`
324 will return the logger first given that name even if the name was
325 used before the `Task` was created.
326 """
327 if not logger:
328 logger = logging.getLogger(name)
329 elif name:
330 logger = logger.getChild(name)
331 return LsstLogAdapter(logger, {})
334LsstLoggers = Union[logging.Logger, LsstLogAdapter]