Coverage for python/lsst/utils/logging.py : 58%

Hot-keys 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")
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")
38class _F:
39 """
40 Format, supporting `str.format()` syntax.
42 Notes
43 -----
44 This follows the recommendation from
45 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects
46 """
47 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any):
48 self.fmt = fmt
49 self.args = args
50 self.kwargs = kwargs
52 def __str__(self) -> str:
53 return self.fmt.format(*self.args, **self.kwargs)
56class LsstLogAdapter(LoggerAdapter):
57 """A special logging adapter to provide log features for LSST code.
59 Expected to be instantiated initially by a call to `getLogger()`.
61 This class provides enhancements over `logging.Logger` that include:
63 * Methods for issuing trace and verbose level log messages.
64 * Provision of a context manager to temporarily change the log level.
65 * Attachment of logging level constants to the class to make it easier
66 for a Task writer to access a specific log level without having to
67 know the underlying logger class.
68 """
70 # Store logging constants in the class for convenience. This is not
71 # something supported by Python loggers but can simplify some
72 # logic if the logger is available.
73 CRITICAL = logging.CRITICAL
74 ERROR = logging.ERROR
75 DEBUG = logging.DEBUG
76 INFO = logging.INFO
77 WARNING = logging.WARNING
79 # Python supports these but prefers they are not used.
80 FATAL = logging.FATAL
81 WARN = logging.WARN
83 # These are specific to Tasks
84 TRACE = TRACE
85 VERBOSE = VERBOSE
87 @contextmanager
88 def temporary_log_level(self, level: Union[int, str]) -> Generator:
89 """A context manager that temporarily sets the level of this logger.
91 Parameters
92 ----------
93 level : `int`
94 The new temporary log level.
95 """
96 old = self.level
97 self.setLevel(level)
98 try:
99 yield
100 finally:
101 self.setLevel(old)
103 @property
104 def level(self) -> int:
105 """Current level of this logger (``int``)."""
106 return self.logger.level
108 def getChild(self, name: str) -> LsstLogAdapter:
109 """Get the named child logger.
111 Parameters
112 ----------
113 name : `str`
114 Name of the child relative to this logger.
116 Returns
117 -------
118 child : `LsstLogAdapter`
119 The child logger.
120 """
121 return getLogger(name=name, logger=self.logger)
123 @deprecated(reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
124 version="v23", category=FutureWarning)
125 def isDebugEnabled(self) -> bool:
126 return self.isEnabledFor(self.DEBUG)
128 @deprecated(reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
129 version="v23", category=FutureWarning)
130 def getName(self) -> str:
131 return self.name
133 @deprecated(reason="Use Python Logger compatible .level property. Will be removed after v23.",
134 version="v23", category=FutureWarning)
135 def getLevel(self) -> int:
136 return self.logger.level
138 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
139 # Python does not provide this method in LoggerAdapter but does
140 # not formally deprecated it in favor of critical() either.
141 # Provide it without deprecation message for consistency with Python.
142 # stacklevel=5 accounts for the forwarding of LoggerAdapter.
143 self.critical(msg, *args, **kwargs, stacklevel=4)
145 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
146 """Issue a VERBOSE level log message.
148 Arguments are as for `logging.info`.
149 ``VERBOSE`` is between ``DEBUG`` and ``INFO``.
150 """
151 # There is no other way to achieve this other than a special logger
152 # method.
153 # Stacklevel is passed in so that the correct line is reported
154 # in the log record and not this line. 3 is this method,
155 # 2 is the level from `self.log` and 1 is the log infrastructure
156 # itself.
157 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
159 def trace(self, fmt: str, *args: Any) -> None:
160 """Issue a TRACE level log message.
162 Arguments are as for `logging.info`.
163 ``TRACE`` is lower than ``DEBUG``.
164 """
165 # There is no other way to achieve this other than a special logger
166 # method. For stacklevel discussion see `verbose()`.
167 self.log(TRACE, fmt, *args, stacklevel=3)
169 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
170 version="v23", category=FutureWarning)
171 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
172 # Stacklevel is 4 to account for the deprecation wrapper
173 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
175 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
176 version="v23", category=FutureWarning)
177 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
178 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
180 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
181 version="v23", category=FutureWarning)
182 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
183 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
185 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
186 version="v23", category=FutureWarning)
187 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
188 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
190 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
191 version="v23", category=FutureWarning)
192 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
193 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
195 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.",
196 version="v23", category=FutureWarning)
197 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
198 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
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("Attempting to set level to %d -- looks like an lsst.log level so scaling it"
211 " accordingly.", level)
212 level //= 1000
214 self.logger.setLevel(level)
216 @property
217 def handlers(self) -> List[logging.Handler]:
218 """Log handlers associated with this logger."""
219 return self.logger.handlers
221 def addHandler(self, handler: logging.Handler) -> None:
222 """Add a handler to this logger.
224 The handler is forwarded to the underlying logger.
225 """
226 self.logger.addHandler(handler)
228 def removeHandler(self, handler: logging.Handler) -> None:
229 """Remove the given handler from the underlying logger."""
230 self.logger.removeHandler(handler)
233def getLogger(name: Optional[str] = None, logger: logging.Logger = None) -> LsstLogAdapter:
234 """Get a logger compatible with LSST usage.
236 Parameters
237 ----------
238 name : `str`, optional
239 Name of the logger. Root logger if `None`.
240 logger : `logging.Logger` or `LsstLogAdapter`
241 If given the logger is converted to the relevant logger class.
242 If ``name`` is given the logger is assumed to be a child of the
243 supplied logger.
245 Returns
246 -------
247 logger : `LsstLogAdapter`
248 The relevant logger.
250 Notes
251 -----
252 A `logging.LoggerAdapter` is used since it is easier to provide a more
253 uniform interface than when using `logging.setLoggerClass`. An adapter
254 can be wrapped around the root logger and the `~logging.setLoggerClass`
255 will return the logger first given that name even if the name was
256 used before the `Task` was created.
257 """
258 if not logger:
259 logger = logging.getLogger(name)
260 elif name:
261 logger = logger.getChild(name)
262 return LsstLogAdapter(logger, {})
265LsstLoggers = Union[logging.Logger, LsstLogAdapter]