Coverage for python/lsst/daf/butler/cli/cliLog.py : 29%

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 daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22import logging
23import os
25try:
26 import lsst.log as lsstLog
27except ModuleNotFoundError:
28 lsstLog = None
31# logging properties
32_LOG_PROP = """\
33log4j.rootLogger=INFO, A1
34log4j.appender.A1=ConsoleAppender
35log4j.appender.A1.Target=System.err
36log4j.appender.A1.layout=PatternLayout
37log4j.appender.A1.layout.ConversionPattern={}
38"""
41class CliLog:
42 """Interface for managing python logging and ``lsst.log``.
44 .. warning::
46 When ``lsst.log`` is importable it is the primary logger, and
47 ``lsst.log`` is set up to be a handler for python logging - so python
48 logging will be processed by ``lsst.log``.
50 This class defines log format strings for the log output and timestamp
51 formats, for both ``lsst.log`` and python logging. If lsst.log is
52 importable then the ``lsstLog_`` format strings will be used, otherwise
53 the ``pylog_`` format strings will be used.
55 This class can perform log uninitialization, which allows command line
56 interface code that initializes logging to run unit tests that execute in
57 batches, without affecting other unit tests. See ``resetLog``."""
59 defaultLsstLogLevel = lsstLog.FATAL if lsstLog is not None else None
61 lsstLog_longLogFmt = "%-5p %d{yyyy-MM-ddTHH:mm:ss.SSSZ} %c (%X{LABEL})(%F:%L)- %m%n"
62 """The log format used when the lsst.log package is importable and the log
63 is initialized with longlog=True."""
65 lsstLog_normalLogFmt = "%c %p: %m%n"
66 """The log format used when the lsst.log package is importable and the log
67 is initialized with longlog=False."""
69 pylog_longLogFmt = "%(levelname)s %(asctime)s %(name)s %(filename)s:%(lineno)s - %(message)s"
70 """The log format used when the lsst.log package is not importable and the
71 log is initialized with longlog=True."""
73 pylog_longLogDateFmt = "%Y-%m-%dT%H:%M:%S%z"
74 """The log date format used when the lsst.log package is not importable and
75 the log is initialized with longlog=True."""
77 pylog_normalFmt = "%(name)s %(levelname)s: %(message)s"
78 """The log format used when the lsst.log package is not importable and the
79 log is initialized with longlog=False."""
81 configState = []
82 """Configuration state. Contains tuples where first item in a tuple is
83 a method and remaining items are arguments for the method.
84 """
86 _initialized = False
87 _lsstLogHandler = None
88 _componentSettings = []
90 @classmethod
91 def initLog(cls, longlog):
92 """Initialize logging. This should only be called once per program
93 execution. After the first call this will log a warning and return.
95 If lsst.log is importable, will add its log handler to the python
96 root logger's handlers.
98 Parameters
99 ----------
100 longlog : `bool`
101 If True, make log messages appear in long format, by default False.
102 """
103 if cls._initialized:
104 # Unit tests that execute more than one command do end up
105 # calling this function multiple times in one program execution,
106 # so do log a debug but don't log an error or fail, just make the
107 # re-initialization a no-op.
108 log = logging.getLogger(__name__)
109 log.debug("Log is already initialized, returning without re-initializing.")
110 return
111 cls._initialized = True
113 if lsstLog is not None:
114 # Initialize global logging config. Skip if the env var
115 # LSST_LOG_CONFIG exists. The file it points to would already
116 # configure lsst.log.
117 if not os.path.isfile(os.environ.get("LSST_LOG_CONFIG", "")):
118 lsstLog.configure_prop(_LOG_PROP.format(
119 cls.lsstLog_longLogFmt if longlog else cls.lsstLog_normalLogFmt))
120 cls._recordComponentSetting(None)
121 pythonLogger = logging.getLogger()
122 pythonLogger.setLevel(logging.INFO)
123 cls._lsstLogHandler = lsstLog.LogHandler()
124 # Forward all Python logging to lsstLog
125 pythonLogger.addHandler(cls._lsstLogHandler)
126 else:
127 cls._recordComponentSetting(None)
128 if longlog:
129 logging.basicConfig(level=logging.INFO,
130 format=cls.pylog_longLogFmt,
131 datefmt=cls.pylog_longLogDateFmt)
132 else:
133 logging.basicConfig(level=logging.INFO, format=cls.pylog_normalFmt)
135 # also capture warnings and send them to logging
136 logging.captureWarnings(True)
138 # remember this call
139 cls.configState.append((cls.initLog, longlog))
141 @classmethod
142 def getHandlerId(cls):
143 """Get the id of the lsst.log handler added to the python logger.
145 Used for unit testing to verify addition & removal of the lsst.log
146 handler.
148 Returns
149 -------
150 `id` or `None`
151 The id of the handler that was added if one was added, or None.
152 """
153 return id(cls._lsstLogHandler)
155 @classmethod
156 def resetLog(cls):
157 """Uninitialize the butler CLI Log handler and reset component log
158 levels.
160 If the lsst.log handler was added to the python root logger's handlers
161 in `initLog`, it will be removed here.
163 For each logger level that was set by this class, sets that logger's
164 level to the value it was before this class set it. For lsst.log, if a
165 component level was uninitialized, it will be set to
166 `Log.defaultLsstLogLevel` because there is no log4cxx api to set a
167 component back to an uninitialized state.
168 """
169 if cls._lsstLogHandler is not None:
170 logging.getLogger().removeHandler(cls._lsstLogHandler)
171 for componentSetting in reversed(cls._componentSettings):
172 if lsstLog is not None and componentSetting.lsstLogLevel is not None:
173 lsstLog.setLevel(componentSetting.component or "", componentSetting.lsstLogLevel)
174 logger = logging.getLogger(componentSetting.component)
175 logger.setLevel(componentSetting.pythonLogLevel)
176 cls._setLogLevel(None, "INFO")
177 cls._initialized = False
178 cls.configState = []
180 @classmethod
181 def setLogLevels(cls, logLevels):
182 """Set log level for one or more components or the root logger.
184 Parameters
185 ----------
186 logLevels : `list` of `tuple`
187 per-component logging levels, each item in the list is a tuple
188 (component, level), `component` is a logger name or an empty string
189 or `None` for root logger, `level` is a logging level name, one of
190 CRITICAL, ERROR, WARNING, INFO, DEBUG (case insensitive).
191 """
192 if isinstance(logLevels, dict):
193 logLevels = logLevels.items()
195 # configure individual loggers
196 for component, level in logLevels:
197 cls._setLogLevel(component, level)
198 # remember this call
199 cls.configState.append((cls._setLogLevel, component, level))
201 @classmethod
202 def _setLogLevel(cls, component, level):
203 """Set the log level for the given component. Record the current log
204 level of the component so that it can be restored when resetting this
205 log.
207 Parameters
208 ----------
209 component : `str` or None
210 The name of the log component or None for the root logger.
211 level : `str`
212 A valid python logging level.
213 """
214 cls._recordComponentSetting(component)
215 if lsstLog is not None:
216 lsstLogger = lsstLog.Log.getLogger(component or "")
217 lsstLogger.setLevel(cls._getLsstLogLevel(level))
218 logging.getLogger(component or None).setLevel(cls._getPyLogLevel(level))
220 @staticmethod
221 def _getPyLogLevel(level):
222 """Get the numeric value for the given log level name.
224 Parameters
225 ----------
226 level : `str`
227 One of the python `logging` log level names.
229 Returns
230 -------
231 numericValue : `int`
232 The python `logging` numeric value for the log level.
233 """
234 return getattr(logging, level, None)
236 @staticmethod
237 def _getLsstLogLevel(level):
238 """Get the numeric value for the given log level name.
240 If `lsst.log` is not setup this function will return `None` regardless
241 of input. `daf_butler` does not directly depend on `lsst.log` and so it
242 will not be setup when `daf_butler` is setup. Packages that depend on
243 `daf_butler` and use `lsst.log` may setup `lsst.log`.
245 Will adapt the python name to an `lsst.log` name:
246 - CRITICAL to FATAL
247 - WARNING to WARN
249 Parameters
250 ----------
251 level : `str`
252 One of the python `logging` log level names.
254 Returns
255 -------
256 numericValue : `int` or `None`
257 The `lsst.log` numeric value.
258 """
259 if lsstLog is None:
260 return None
261 if level == "CRITICAL":
262 level = "FATAL"
263 elif level == "WARNING":
264 level = "WARN"
265 return getattr(lsstLog.Log, level, None)
267 class ComponentSettings:
268 """Container for log level values for a logging component."""
269 def __init__(self, component):
270 self.component = component
271 self.pythonLogLevel = logging.getLogger(component).level
272 self.lsstLogLevel = (lsstLog.Log.getLogger(component or "").getLevel()
273 if lsstLog is not None else None)
274 if self.lsstLogLevel == -1:
275 self.lsstLogLevel = CliLog.defaultLsstLogLevel
277 def __repr__(self):
278 return (f"ComponentSettings(component={self.component}, pythonLogLevel={self.pythonLogLevel}, "
279 f"lsstLogLevel={self.lsstLogLevel})")
281 @classmethod
282 def _recordComponentSetting(cls, component):
283 """Cache current levels for the given component in the list of
284 component levels."""
285 componentSettings = cls.ComponentSettings(component)
286 cls._componentSettings.append(componentSettings)
288 @classmethod
289 def replayConfigState(cls, configState):
290 """Re-create configuration using configuration state recorded earlier.
292 Parameters
293 ----------
294 configState : `list` of `tuple`
295 Tuples contain a method as first item and arguments for the method,
296 in the same format as ``cls.configState``.
297 """
298 if cls._initialized or cls.configState:
299 # Already initialized, do not touch anything.
300 log = logging.getLogger(__name__)
301 log.warning("Log is already initialized, will not replay configuration.")
302 return
304 # execute each one in order
305 for call in configState:
306 method, *args = call
307 method(*args)