Hide keyboard shortcuts

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/>. 

21 

22import logging 

23import os 

24 

25try: 

26 import lsst.log as lsstLog 

27except ModuleNotFoundError: 

28 lsstLog = None 

29 

30 

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""" 

39 

40 

41class CliLog: 

42 """Interface for managing python logging and ``lsst.log``. 

43 

44 .. warning:: 

45 

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``. 

49 

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. 

54 

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``.""" 

58 

59 defaultLsstLogLevel = lsstLog.FATAL if lsstLog is not None else None 

60 

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.""" 

64 

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.""" 

68 

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.""" 

72 

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.""" 

76 

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.""" 

80 

81 _initialized = False 

82 _lsstLogHandler = None 

83 _componentSettings = [] 

84 

85 @classmethod 

86 def initLog(cls, longlog): 

87 """Initialize logging. This should only be called once per program 

88 execution. After the first call this will log a warning and return. 

89 

90 If lsst.log is importable, will add its log handler to the python 

91 root logger's handlers. 

92 

93 Parameters 

94 ---------- 

95 longlog : `bool` 

96 If True, make log messages appear in long format, by default False. 

97 """ 

98 if cls._initialized: 

99 # Unit tests that execute more than one command do end up 

100 # calling this function multiple times in one program execution, 

101 # so do log a debug but don't log an error or fail, just make the 

102 # re-initialization a no-op. 

103 log = logging.getLogger(__name__.partition(".")[2]) 

104 log.debug("Log is already initialized, returning without re-initializing.") 

105 return 

106 cls._initialized = True 

107 

108 if lsstLog is not None: 

109 # Initialize global logging config. Skip if the env var 

110 # LSST_LOG_CONFIG exists. The file it points to would already 

111 # configure lsst.log. 

112 if not os.path.isfile(os.environ.get("LSST_LOG_CONFIG", "")): 

113 lsstLog.configure_prop(_LOG_PROP.format( 

114 cls.lsstLog_longLogFmt if longlog else cls.lsstLog_normalLogFmt)) 

115 cls._recordComponentSetting(None) 

116 pythonLogger = logging.getLogger() 

117 pythonLogger.setLevel(logging.INFO) 

118 cls._lsstLogHandler = lsstLog.LogHandler() 

119 # Forward all Python logging to lsstLog 

120 pythonLogger.addHandler(cls._lsstLogHandler) 

121 else: 

122 cls._recordComponentSetting(None) 

123 if longlog: 

124 logging.basicConfig(level=logging.INFO, 

125 format=cls.pylog_longLogFmt, 

126 datefmt=cls.pylog_longLogDateFmt) 

127 else: 

128 logging.basicConfig(level=logging.INFO, format=cls.pylog_normalFmt) 

129 

130 # also capture warnings and send them to logging 

131 logging.captureWarnings(True) 

132 

133 @classmethod 

134 def getHandlerId(cls): 

135 """Get the id of the lsst.log handler added to the python logger. 

136 

137 Used for unit testing to verify addition & removal of the lsst.log 

138 handler. 

139 

140 Returns 

141 ------- 

142 `id` or `None` 

143 The id of the handler that was added if one was added, or None. 

144 """ 

145 return id(cls._lsstLogHandler) 

146 

147 @classmethod 

148 def resetLog(cls): 

149 """Uninitialize the butler CLI Log handler and reset component log 

150 levels. 

151 

152 If the lsst.log handler was added to the python root logger's handlers 

153 in `initLog`, it will be removed here. 

154 

155 For each logger level that was set by this class, sets that logger's 

156 level to the value it was before this class set it. For lsst.log, if a 

157 component level was uninitialized, it will be set to 

158 `Log.defaultLsstLogLevel` because there is no log4cxx api to set a 

159 component back to an uninitialized state. 

160 """ 

161 if cls._lsstLogHandler is not None: 

162 logging.getLogger().removeHandler(cls._lsstLogHandler) 

163 for componentSetting in reversed(cls._componentSettings): 

164 if lsstLog is not None and componentSetting.lsstLogLevel is not None: 

165 lsstLog.setLevel(componentSetting.component or "", componentSetting.lsstLogLevel) 

166 logger = logging.getLogger(componentSetting.component) 

167 logger.setLevel(componentSetting.pythonLogLevel) 

168 cls._setLogLevel(None, "INFO") 

169 cls._initialized = False 

170 

171 @classmethod 

172 def setLogLevels(cls, logLevels): 

173 """Set log level for one or more components or the root logger. 

174 

175 Parameters 

176 ---------- 

177 logLevels : `list` of `tuple` 

178 per-component logging levels, each item in the list is a tuple 

179 (component, level), `component` is a logger name or an empty string 

180 or `None` for root logger, `level` is a logging level name, one of 

181 CRITICAL, ERROR, WARNING, INFO, DEBUG (case insensitive). 

182 """ 

183 if isinstance(logLevels, dict): 

184 logLevels = logLevels.items() 

185 

186 # configure individual loggers 

187 for component, level in logLevels: 

188 cls._setLogLevel(component, level) 

189 

190 @classmethod 

191 def _setLogLevel(cls, component, level): 

192 """Set the log level for the given component. Record the current log 

193 level of the component so that it can be restored when resetting this 

194 log. 

195 

196 Parameters 

197 ---------- 

198 component : `str` or None 

199 The name of the log component or None for the root logger. 

200 level : `str` 

201 A valid python logging level. 

202 """ 

203 cls._recordComponentSetting(component) 

204 if lsstLog is not None: 

205 lsstLogger = lsstLog.Log.getLogger(component or "") 

206 lsstLogger.setLevel(cls._getLsstLogLevel(level)) 

207 logging.getLogger(component or None).setLevel(cls._getPyLogLevel(level)) 

208 

209 @staticmethod 

210 def _getPyLogLevel(level): 

211 """Get the numeric value for the given log level name. 

212 

213 Parameters 

214 ---------- 

215 level : `str` 

216 One of the python `logging` log level names. 

217 

218 Returns 

219 ------- 

220 numericValue : `int` 

221 The python `logging` numeric value for the log level. 

222 """ 

223 return getattr(logging, level, None) 

224 

225 @staticmethod 

226 def _getLsstLogLevel(level): 

227 """Get the numeric value for the given log level name. 

228 

229 If `lsst.log` is not setup this function will return `None` regardless 

230 of input. `daf_butler` does not directly depend on `lsst.log` and so it 

231 will not be setup when `daf_butler` is setup. Packages that depend on 

232 `daf_butler` and use `lsst.log` may setup `lsst.log`. 

233 

234 Will adapt the python name to an `lsst.log` name: 

235 - CRITICAL to FATAL 

236 - WARNING to WARN 

237 

238 Parameters 

239 ---------- 

240 level : `str` 

241 One of the python `logging` log level names. 

242 

243 Returns 

244 ------- 

245 numericValue : `int` or `None` 

246 The `lsst.log` numeric value. 

247 """ 

248 if lsstLog is None: 

249 return None 

250 if level == "CRITICAL": 

251 level = "FATAL" 

252 elif level == "WARNING": 

253 level = "WARN" 

254 return getattr(lsstLog.Log, level, None) 

255 

256 class ComponentSettings: 

257 """Container for log level values for a logging component.""" 

258 def __init__(self, component): 

259 self.component = component 

260 self.pythonLogLevel = logging.getLogger(component).level 

261 self.lsstLogLevel = (lsstLog.Log.getLogger(component or "").getLevel() 

262 if lsstLog is not None else None) 

263 if self.lsstLogLevel == -1: 

264 self.lsstLogLevel = CliLog.defaultLsstLogLevel 

265 

266 def __repr__(self): 

267 return (f"ComponentSettings(component={self.component}, pythonLogLevel={self.pythonLogLevel}, " 

268 f"lsstLogLevel={self.lsstLogLevel})") 

269 

270 @classmethod 

271 def _recordComponentSetting(cls, component): 

272 """Cache current levels for the given component in the list of 

273 component levels.""" 

274 componentSettings = cls.ComponentSettings(component) 

275 cls._componentSettings.append(componentSettings)