Coverage for python/lsst/utils/logging.py: 55%
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 logging import LoggerAdapter
18from deprecated.sphinx import deprecated
19from contextlib import contextmanager
21from typing import (
22 Any,
23 Generator,
24 List,
25 Optional,
26 Union,
27)
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 """Adjusts 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 Notes
50 -----
51 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
52 the specified threshold are set to ``INFO`` and those below the threshold
53 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
54 issue ``DEBUG`` log messages.
56 Examples
57 --------
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``.
65 """
66 for i in range(6):
67 level = logging.INFO if i > number else logging.DEBUG
68 log_name = f"TRACE{i}.{name}" if name else f"TRACE{i}"
69 logging.getLogger(log_name).setLevel(level)
72class _F:
73 """
74 Format, supporting `str.format()` syntax.
76 Notes
77 -----
78 This follows the recommendation from
79 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
80 """
81 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
82 self.fmt = fmt
83 self.args = args
84 self.kwargs = kwargs
86 def __str__(self) -> str:
87 return self.fmt.format(*self.args, **self.kwargs)
90class LsstLogAdapter(LoggerAdapter):
91 """A special logging adapter to provide log features for LSST code.
93 Expected to be instantiated initially by a call to `getLogger()`.
95 This class provides enhancements over `logging.Logger` that include:
97 * Methods for issuing trace and verbose level log messages.
98 * Provision of a context manager to temporarily change the log level.
99 * Attachment of logging level constants to the class to make it easier
100 for a Task writer to access a specific log level without having to
101 know the underlying logger class.
102 """
104 # Store logging constants in the class for convenience. This is not
105 # something supported by Python loggers but can simplify some
106 # logic if the logger is available.
107 CRITICAL = logging.CRITICAL
108 ERROR = logging.ERROR
109 DEBUG = logging.DEBUG
110 INFO = logging.INFO
111 WARNING = logging.WARNING
113 # Python supports these but prefers they are not used.
114 FATAL = logging.FATAL
115 WARN = logging.WARN
117 # These are specific to Tasks
118 TRACE = TRACE
119 VERBOSE = VERBOSE
121 @contextmanager
122 def temporary_log_level(self, level: Union[int, str]) -> Generator:
123 """A context manager that temporarily sets the level of this logger.
125 Parameters
126 ----------
127 level : `int`
128 The new temporary log level.
129 """
130 old = self.level
131 self.setLevel(level)
132 try:
133 yield
134 finally:
135 self.setLevel(old)
137 @property
138 def level(self) -> int:
139 """Current level of this logger (``int``)."""
140 return self.logger.level
142 def getChild(self, name: str) -> LsstLogAdapter:
143 """Get the named child logger.
145 Parameters
146 ----------
147 name : `str`
148 Name of the child relative to this logger.
150 Returns
151 -------
152 child : `LsstLogAdapter`
153 The child logger.
154 """
155 return getLogger(name=name, logger=self.logger)
157 @deprecated(reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
158 version="v23", category=FutureWarning)
159 def isDebugEnabled(self) -> bool:
160 return self.isEnabledFor(self.DEBUG)
162 @deprecated(reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
163 version="v23", category=FutureWarning)
164 def getName(self) -> str:
165 return self.name
167 @deprecated(reason="Use Python Logger compatible .level property. Will be removed after v23.",
168 version="v23", category=FutureWarning)
169 def getLevel(self) -> int:
170 return self.logger.level
172 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
173 # Python does not provide this method in LoggerAdapter but does
174 # not formally deprecated it in favor of critical() either.
175 # Provide it without deprecation message for consistency with Python.
176 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
177 self.critical(msg, *args, **kwargs, stacklevel=4)
179 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
180 """Issue a VERBOSE level log message.
182 Arguments are as for `logging.info`.
183 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
184 """
185 # There is no other way to achieve this other than a special logger
186 # method.
187 # Stacklevel is passed in so that the correct line is reported
188 # in the log record and not this line. 3 is this method,
189 # 2 is the level from `self.log` and 1 is the log infrastructure
190 # itself.
191 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
193 def trace(self, fmt: str, *args: Any) -> None:
194 """Issue a TRACE level log message.
196 Arguments are as for `logging.info`.
197 ``TRACE`` is lower than ``DEBUG``.
198 """
199 # There is no other way to achieve this other than a special logger
200 # method. For stacklevel discussion see `verbose()`.
201 self.log(TRACE, fmt, *args, stacklevel=3)
203 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
204 version="v23", category=FutureWarning)
205 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
206 # Stacklevel is 4 to account for the deprecation wrapper
207 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
209 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
210 version="v23", category=FutureWarning)
211 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
212 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
214 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
215 version="v23", category=FutureWarning)
216 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
217 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
219 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
220 version="v23", category=FutureWarning)
221 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
222 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
224 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
225 version="v23", category=FutureWarning)
226 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
227 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
229 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
230 version="v23", category=FutureWarning)
231 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
232 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
234 def setLevel(self, level: Union[int, str]) -> None:
235 """Set the level for the logger, trapping lsst.log values.
237 Parameters
238 ----------
239 level : `int`
240 The level to use. If the level looks too big to be a Python
241 logging level it is assumed to be a lsst.log level.
242 """
243 if isinstance(level, int) and level > logging.CRITICAL:
244 self.logger.warning("Attempting to set level to %d -- looks like an lsst.log level so scaling it"
245 " accordingly.", level)
246 level //= 1000
248 self.logger.setLevel(level)
250 @property
251 def handlers(self) -> List[logging.Handler]:
252 """Log handlers associated with this logger."""
253 return self.logger.handlers
255 def addHandler(self, handler: logging.Handler) -> None:
256 """Add a handler to this logger.
258 The handler is forwarded to the underlying logger.
259 """
260 self.logger.addHandler(handler)
262 def removeHandler(self, handler: logging.Handler) -> None:
263 """Remove the given handler from the underlying logger."""
264 self.logger.removeHandler(handler)
267def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
268 """Get a logger compatible with LSST usage.
270 Parameters
271 ----------
272 name : `str`, optional
273 Name of the logger. Root logger if `None`.
274 logger : `logging.Logger` or `LsstLogAdapter`
275 If given the logger is converted to the relevant logger class.
276 If ``name`` is given the logger is assumed to be a child of the
277 supplied logger.
279 Returns
280 -------
281 logger : `LsstLogAdapter`
282 The relevant logger.
284 Notes
285 -----
286 A `logging.LoggerAdapter` is used since it is easier to provide a more
287 uniform interface than when using `logging.setLoggerClass`. An adapter
288 can be wrapped around the root logger and the `~logging.setLoggerClass`
289 will return the logger first given that name even if the name was
290 used before the `Task` was created.
291 """
292 if not logger:
293 logger = logging.getLogger(name)
294 elif name:
295 logger = logger.getChild(name)
296 return LsstLogAdapter(logger, {})
299LsstLoggers = Union[logging.Logger, LsstLogAdapter]