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 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)
29try:
30 import lsst.log.utils as logUtils
31except ImportError:
32 logUtils = None
34# log level for trace (verbose debug).
35TRACE = 5
36logging.addLevelName(TRACE, "TRACE")
38# Verbose logging is midway between INFO and DEBUG.
39VERBOSE = (logging.INFO + logging.DEBUG) // 2
40logging.addLevelName(VERBOSE, "VERBOSE")
43def trace_set_at(name: str, number: int) -> None:
44 """Adjust logging level to display messages with the trace number being
45 less than or equal to the provided value.
47 Parameters
48 ----------
49 name : `str`
50 Name of the logger.
51 number : `int`
52 The trace number threshold for display.
54 Examples
55 --------
56 .. code-block:: python
58 lsst.utils.logging.trace_set_at("lsst.afw", 3)
60 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to
61 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``.
63 Notes
64 -----
65 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above
66 the specified threshold are set to ``INFO`` and those below the threshold
67 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only
68 issue ``DEBUG`` log messages.
70 If ``lsst.log`` is installed, this function will also call
71 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are
72 also configured correctly.
73 """
74 for i in range(6):
75 level = logging.INFO if i > number else logging.DEBUG
76 log_name = f"TRACE{i}.{name}" if name else f"TRACE{i}"
77 logging.getLogger(log_name).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 @deprecated(reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
170 version="v23", category=FutureWarning)
171 def isDebugEnabled(self) -> bool:
172 return self.isEnabledFor(self.DEBUG)
174 @deprecated(reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
175 version="v23", category=FutureWarning)
176 def getName(self) -> str:
177 return self.name
179 @deprecated(reason="Use Python Logger compatible .level property. Will be removed after v23.",
180 version="v23", category=FutureWarning)
181 def getLevel(self) -> int:
182 return self.logger.level
184 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
185 # Python does not provide this method in LoggerAdapter but does
186 # not formally deprecated it in favor of critical() either.
187 # Provide it without deprecation message for consistency with Python.
188 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
189 self.critical(msg, *args, **kwargs, stacklevel=4)
191 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
192 """Issue a VERBOSE level log message.
194 Arguments are as for `logging.info`.
195 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
196 """
197 # There is no other way to achieve this other than a special logger
198 # method.
199 # Stacklevel is passed in so that the correct line is reported
200 # in the log record and not this line. 3 is this method,
201 # 2 is the level from `self.log` and 1 is the log infrastructure
202 # itself.
203 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
205 def trace(self, fmt: str, *args: Any) -> None:
206 """Issue a TRACE level log message.
208 Arguments are as for `logging.info`.
209 ``TRACE`` is lower than ``DEBUG``.
210 """
211 # There is no other way to achieve this other than a special logger
212 # method. For stacklevel discussion see `verbose()`.
213 self.log(TRACE, fmt, *args, stacklevel=3)
215 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
216 version="v23", category=FutureWarning)
217 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
218 # Stacklevel is 4 to account for the deprecation wrapper
219 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
221 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
222 version="v23", category=FutureWarning)
223 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
224 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
226 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
227 version="v23", category=FutureWarning)
228 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
229 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
231 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
232 version="v23", category=FutureWarning)
233 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
234 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
236 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
237 version="v23", category=FutureWarning)
238 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
239 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
241 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
242 version="v23", category=FutureWarning)
243 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
244 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
246 def setLevel(self, level: Union[int, str]) -> None:
247 """Set the level for the logger, trapping lsst.log values.
249 Parameters
250 ----------
251 level : `int`
252 The level to use. If the level looks too big to be a Python
253 logging level it is assumed to be a lsst.log level.
254 """
255 if isinstance(level, int) and level > logging.CRITICAL:
256 self.logger.warning("Attempting to set level to %d -- looks like an lsst.log level so scaling it"
257 " accordingly.", level)
258 level //= 1000
260 self.logger.setLevel(level)
262 @property
263 def handlers(self) -> List[logging.Handler]:
264 """Log handlers associated with this logger."""
265 return self.logger.handlers
267 def addHandler(self, handler: logging.Handler) -> None:
268 """Add a handler to this logger.
270 The handler is forwarded to the underlying logger.
271 """
272 self.logger.addHandler(handler)
274 def removeHandler(self, handler: logging.Handler) -> None:
275 """Remove the given handler from the underlying logger."""
276 self.logger.removeHandler(handler)
279def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
280 """Get a logger compatible with LSST usage.
282 Parameters
283 ----------
284 name : `str`, optional
285 Name of the logger. Root logger if `None`.
286 logger : `logging.Logger` or `LsstLogAdapter`
287 If given the logger is converted to the relevant logger class.
288 If ``name`` is given the logger is assumed to be a child of the
289 supplied logger.
291 Returns
292 -------
293 logger : `LsstLogAdapter`
294 The relevant logger.
296 Notes
297 -----
298 A `logging.LoggerAdapter` is used since it is easier to provide a more
299 uniform interface than when using `logging.setLoggerClass`. An adapter
300 can be wrapped around the root logger and the `~logging.setLoggerClass`
301 will return the logger first given that name even if the name was
302 used before the `Task` was created.
303 """
304 if not logger:
305 logger = logging.getLogger(name)
306 elif name:
307 logger = logger.getChild(name)
308 return LsstLogAdapter(logger, {})
311LsstLoggers = Union[logging.Logger, LsstLogAdapter]