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

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
24try:
25 import lsst.log as lsstLog
26except ModuleNotFoundError:
27 lsstLog = None
30# logging properties
31_LOG_PROP = """\
32log4j.rootLogger=INFO, A1
33log4j.appender.A1=ConsoleAppender
34log4j.appender.A1.Target=System.err
35log4j.appender.A1.layout=PatternLayout
36log4j.appender.A1.layout.ConversionPattern={}
37"""
40class CliLog:
42 defaultLsstLogLevel = lsstLog.FATAL if lsstLog is not None else None
44 longLogFmt = "%-5p %d{yyyy-MM-ddTHH:mm:ss.SSSZ} %c (%X{LABEL})(%F:%L)- %m%n"
45 normalLogFmt = "%c %p: %m%n"
47 _initialized = False
48 _lsstLogHandler = None
49 _componentSettings = []
51 @classmethod
52 def initLog(cls, longlog):
53 """Initialize logging. This should only be called once per program
54 execution. After the first call this will log a warning and return.
56 If lsst.log is importable, will add its log handler to the python
57 root logger's handlers.
59 Parameters
60 ----------
61 longlog : `bool`
62 If True, make log messages appear in long format, by default False.
63 """
64 if cls._initialized:
65 # Unit tests that execute more than one command do end up
66 # calling this fucntion multiple times in one program execution,
67 # so do log a debug but don't log an error or fail, just make the
68 # re-initalization a no-op.
69 log = logging.getLogger(__name__.partition(".")[2])
70 log.debug("Log is already initialized, returning without re-initializing.")
71 return
72 cls._initialized = True
74 if lsstLog is not None:
75 # global logging config
76 lsstLog.configure_prop(_LOG_PROP.format(cls.longLogFmt if longlog else cls.normalLogFmt))
77 cls._recordComponentSetting(None)
78 pythonLogger = logging.getLogger()
79 pythonLogger.setLevel(logging.INFO)
80 cls._lsstLogHandler = lsstLog.LogHandler()
81 # Forward all Python logging to lsstLog
82 pythonLogger.addHandler(cls._lsstLogHandler)
83 else:
84 cls._recordComponentSetting(None)
85 logging.basicConfig(level=logging.INFO)
87 # also capture warnings and send them to logging
88 logging.captureWarnings(True)
90 @classmethod
91 def getHandlerId(cls):
92 """Get the id of the lsst.log handler added to the python logger.
94 Used for unit testing to verify addition & removal of the lsst.log
95 handler.
97 Returns
98 -------
99 `id` or `None`
100 The id of the handler that was added if one was added, or None.
101 """
102 return id(cls._lsstLogHandler)
104 @classmethod
105 def resetLog(cls):
106 """Uninitialize the butler CLI Log handler and reset component log
107 levels.
109 If the lsst.log handler was added to the python root logger's handlers
110 in `initLog`, it will be removed here.
112 For each loger level that was set by this class, sets that logger's
113 level to the value it was before this class set it. For lsst.log, if a
114 component level was uninitialzed, it will be set to
115 `Log.defaultLsstLogLevel` because there is no log4cxx api to set a
116 component back to an uninitialized state.
117 """
118 if cls._lsstLogHandler is not None:
119 logging.getLogger().removeHandler(cls._lsstLogHandler)
120 for componentSetting in reversed(cls._componentSettings):
121 if lsstLog is not None and componentSetting.lsstLogLevel is not None:
122 lsstLog.setLevel(componentSetting.component or "", componentSetting.lsstLogLevel)
123 logger = logging.getLogger(componentSetting.component)
124 logger.setLevel(componentSetting.pythonLogLevel)
125 cls._setLogLevel(None, "INFO")
126 cls._initialized = False
128 @classmethod
129 def setLogLevels(cls, logLevels):
130 """Set log level for one or more components or the root logger.
132 Parameters
133 ----------
134 logLevels : `list` of `tuple`
135 per-component logging levels, each item in the list is a tuple
136 (component, level), `component` is a logger name or an empty string
137 or `None` for root logger, `level` is a logging level name, one of
138 CRITICAL, ERROR, WARNING, INFO, DEBUG (case insensitive).
139 """
140 if isinstance(logLevels, dict):
141 logLevels = logLevels.items()
143 # configure individual loggers
144 for component, level in logLevels:
145 cls._setLogLevel(component, level)
147 @classmethod
148 def _setLogLevel(cls, component, level):
149 """Set the log level for the given component. Record the current log
150 level of the component so that it can be restored when resetting this
151 log.
153 Parameters
154 ----------
155 component : `str` or None
156 The name of the log component or None for the root logger.
157 level : `str`
158 A valid python logging level.
159 """
160 cls._recordComponentSetting(component)
161 if lsstLog is not None:
162 lsstLogger = lsstLog.Log.getLogger(component or "")
163 lsstLogger.setLevel(cls._getLsstLogLevel(level))
164 logging.getLogger(component or None).setLevel(cls._getPyLogLevel(level))
166 @staticmethod
167 def _getPyLogLevel(level):
168 """Get the numeric value for the given log level name.
170 Parameters
171 ----------
172 level : `str`
173 One of the python `logging` log level names.
175 Returns
176 -------
177 numericValue : `int`
178 The python `logging` numeric value for the log level.
179 """
180 return getattr(logging, level, None)
182 @staticmethod
183 def _getLsstLogLevel(level):
184 """Get the numeric value for the given log level name.
186 If `lsst.log` is not setup this function will return `None` regardless
187 of input. `daf_butler` does not directly depend on `lsst.log` and so it
188 will not be setup when `daf_butler` is setup. Packages that depend on
189 `daf_butler` and use `lsst.log` may setup `lsst.log`.
191 Will adapt the python name to an `lsst.log` name:
192 - CRITICAL to FATAL
193 - WARNING to WARN
195 Parameters
196 ----------
197 level : `str`
198 One of the python `logging` log level names.
200 Returns
201 -------
202 numericValue : `int` or `None`
203 The `lsst.log` numeric value.
204 """
205 if lsstLog is None:
206 return None
207 if level == "CRITICAL":
208 level = "FATAL"
209 elif level == "WARNING":
210 level = "WARN"
211 return getattr(lsstLog.Log, level, None)
213 class ComponentSettings:
214 """Container for log level values for a logging component."""
215 def __init__(self, component):
216 self.component = component
217 self.pythonLogLevel = logging.getLogger(component).level
218 self.lsstLogLevel = (lsstLog.Log.getLogger(component or "").getLevel()
219 if lsstLog is not None else None)
220 if self.lsstLogLevel == -1:
221 self.lsstLogLevel = CliLog.defaultLsstLogLevel
223 def __repr__(self):
224 return (f"ComponentSettings(component={self.component}, pythonLogLevel={self.pythonLogLevel}, "
225 f"lsstLogLevel={self.lsstLogLevel})")
227 @classmethod
228 def _recordComponentSetting(cls, component):
229 """Cache current levels for the given component in the list of
230 component levels."""
231 componentSettings = cls.ComponentSettings(component)
232 cls._componentSettings.append(componentSettings)