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

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. Handles
43 initialization so that lsst.log is a handler for python logging.
45 Handles log uninitialization, which allows command line interface code that
46 initializes logging to run unit tests that execute in batches, without
47 affecting other unit tests. """
49 defaultLsstLogLevel = lsstLog.FATAL if lsstLog is not None else None
51 longLogFmt = "%-5p %d{yyyy-MM-ddTHH:mm:ss.SSSZ} %c (%X{LABEL})(%F:%L)- %m%n"
52 normalLogFmt = "%c %p: %m%n"
54 _initialized = False
55 _lsstLogHandler = None
56 _componentSettings = []
58 @classmethod
59 def initLog(cls, longlog):
60 """Initialize logging. This should only be called once per program
61 execution. After the first call this will log a warning and return.
63 If lsst.log is importable, will add its log handler to the python
64 root logger's handlers.
66 Parameters
67 ----------
68 longlog : `bool`
69 If True, make log messages appear in long format, by default False.
70 """
71 if cls._initialized:
72 # Unit tests that execute more than one command do end up
73 # calling this function multiple times in one program execution,
74 # so do log a debug but don't log an error or fail, just make the
75 # re-initialization a no-op.
76 log = logging.getLogger(__name__.partition(".")[2])
77 log.debug("Log is already initialized, returning without re-initializing.")
78 return
79 cls._initialized = True
81 if lsstLog is not None:
82 # Initialize global logging config. Skip if the env var
83 # LSST_LOG_CONFIG exists. The file it points to would already
84 # configure lsst.log.
85 if not os.path.isfile(os.environ.get("LSST_LOG_CONFIG", "")):
86 lsstLog.configure_prop(_LOG_PROP.format(cls.longLogFmt if longlog else cls.normalLogFmt))
87 cls._recordComponentSetting(None)
88 pythonLogger = logging.getLogger()
89 pythonLogger.setLevel(logging.INFO)
90 cls._lsstLogHandler = lsstLog.LogHandler()
91 # Forward all Python logging to lsstLog
92 pythonLogger.addHandler(cls._lsstLogHandler)
93 else:
94 cls._recordComponentSetting(None)
95 logging.basicConfig(level=logging.INFO)
97 # also capture warnings and send them to logging
98 logging.captureWarnings(True)
100 @classmethod
101 def getHandlerId(cls):
102 """Get the id of the lsst.log handler added to the python logger.
104 Used for unit testing to verify addition & removal of the lsst.log
105 handler.
107 Returns
108 -------
109 `id` or `None`
110 The id of the handler that was added if one was added, or None.
111 """
112 return id(cls._lsstLogHandler)
114 @classmethod
115 def resetLog(cls):
116 """Uninitialize the butler CLI Log handler and reset component log
117 levels.
119 If the lsst.log handler was added to the python root logger's handlers
120 in `initLog`, it will be removed here.
122 For each logger level that was set by this class, sets that logger's
123 level to the value it was before this class set it. For lsst.log, if a
124 component level was uninitialized, it will be set to
125 `Log.defaultLsstLogLevel` because there is no log4cxx api to set a
126 component back to an uninitialized state.
127 """
128 if cls._lsstLogHandler is not None:
129 logging.getLogger().removeHandler(cls._lsstLogHandler)
130 for componentSetting in reversed(cls._componentSettings):
131 if lsstLog is not None and componentSetting.lsstLogLevel is not None:
132 lsstLog.setLevel(componentSetting.component or "", componentSetting.lsstLogLevel)
133 logger = logging.getLogger(componentSetting.component)
134 logger.setLevel(componentSetting.pythonLogLevel)
135 cls._setLogLevel(None, "INFO")
136 cls._initialized = False
138 @classmethod
139 def setLogLevels(cls, logLevels):
140 """Set log level for one or more components or the root logger.
142 Parameters
143 ----------
144 logLevels : `list` of `tuple`
145 per-component logging levels, each item in the list is a tuple
146 (component, level), `component` is a logger name or an empty string
147 or `None` for root logger, `level` is a logging level name, one of
148 CRITICAL, ERROR, WARNING, INFO, DEBUG (case insensitive).
149 """
150 if isinstance(logLevels, dict):
151 logLevels = logLevels.items()
153 # configure individual loggers
154 for component, level in logLevels:
155 cls._setLogLevel(component, level)
157 @classmethod
158 def _setLogLevel(cls, component, level):
159 """Set the log level for the given component. Record the current log
160 level of the component so that it can be restored when resetting this
161 log.
163 Parameters
164 ----------
165 component : `str` or None
166 The name of the log component or None for the root logger.
167 level : `str`
168 A valid python logging level.
169 """
170 cls._recordComponentSetting(component)
171 if lsstLog is not None:
172 lsstLogger = lsstLog.Log.getLogger(component or "")
173 lsstLogger.setLevel(cls._getLsstLogLevel(level))
174 logging.getLogger(component or None).setLevel(cls._getPyLogLevel(level))
176 @staticmethod
177 def _getPyLogLevel(level):
178 """Get the numeric value for the given log level name.
180 Parameters
181 ----------
182 level : `str`
183 One of the python `logging` log level names.
185 Returns
186 -------
187 numericValue : `int`
188 The python `logging` numeric value for the log level.
189 """
190 return getattr(logging, level, None)
192 @staticmethod
193 def _getLsstLogLevel(level):
194 """Get the numeric value for the given log level name.
196 If `lsst.log` is not setup this function will return `None` regardless
197 of input. `daf_butler` does not directly depend on `lsst.log` and so it
198 will not be setup when `daf_butler` is setup. Packages that depend on
199 `daf_butler` and use `lsst.log` may setup `lsst.log`.
201 Will adapt the python name to an `lsst.log` name:
202 - CRITICAL to FATAL
203 - WARNING to WARN
205 Parameters
206 ----------
207 level : `str`
208 One of the python `logging` log level names.
210 Returns
211 -------
212 numericValue : `int` or `None`
213 The `lsst.log` numeric value.
214 """
215 if lsstLog is None:
216 return None
217 if level == "CRITICAL":
218 level = "FATAL"
219 elif level == "WARNING":
220 level = "WARN"
221 return getattr(lsstLog.Log, level, None)
223 class ComponentSettings:
224 """Container for log level values for a logging component."""
225 def __init__(self, component):
226 self.component = component
227 self.pythonLogLevel = logging.getLogger(component).level
228 self.lsstLogLevel = (lsstLog.Log.getLogger(component or "").getLevel()
229 if lsstLog is not None else None)
230 if self.lsstLogLevel == -1:
231 self.lsstLogLevel = CliLog.defaultLsstLogLevel
233 def __repr__(self):
234 return (f"ComponentSettings(component={self.component}, pythonLogLevel={self.pythonLogLevel}, "
235 f"lsstLogLevel={self.lsstLogLevel})")
237 @classmethod
238 def _recordComponentSetting(cls, component):
239 """Cache current levels for the given component in the list of
240 component levels."""
241 componentSettings = cls.ComponentSettings(component)
242 cls._componentSettings.append(componentSettings)