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

85 

86 _initialized = False 

87 _lsstLogHandler = None 

88 _componentSettings = [] 

89 

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. 

94 

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

96 root logger's handlers. 

97 

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 

112 

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) 

134 

135 # also capture warnings and send them to logging 

136 logging.captureWarnings(True) 

137 

138 # remember this call 

139 cls.configState.append((cls.initLog, longlog)) 

140 

141 @classmethod 

142 def getHandlerId(cls): 

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

144 

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

146 handler. 

147 

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) 

154 

155 @classmethod 

156 def resetLog(cls): 

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

158 levels. 

159 

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

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

162 

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 = [] 

179 

180 @classmethod 

181 def setLogLevels(cls, logLevels): 

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

183 

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() 

194 

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

200 

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. 

206 

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

219 

220 @staticmethod 

221 def _getPyLogLevel(level): 

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

223 

224 Parameters 

225 ---------- 

226 level : `str` 

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

228 

229 Returns 

230 ------- 

231 numericValue : `int` 

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

233 """ 

234 if level == "VERBOSE": 

235 from .. import VERBOSE 

236 return VERBOSE 

237 return getattr(logging, level, None) 

238 

239 @staticmethod 

240 def _getLsstLogLevel(level): 

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

242 

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

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

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

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

247 

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

249 - CRITICAL to FATAL 

250 - WARNING to WARN 

251 

252 Parameters 

253 ---------- 

254 level : `str` 

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

256 

257 Returns 

258 ------- 

259 numericValue : `int` or `None` 

260 The `lsst.log` numeric value. 

261 """ 

262 if lsstLog is None: 

263 return None 

264 if level == "CRITICAL": 

265 level = "FATAL" 

266 elif level == "WARNING": 

267 level = "WARN" 

268 elif level == "VERBOSE": 

269 # LSST log does not yet have verbose defined 

270 return (lsstLog.Log.DEBUG + lsstLog.Log.INFO) // 2 

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

272 

273 class ComponentSettings: 

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

275 def __init__(self, component): 

276 self.component = component 

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

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

279 if lsstLog is not None else None) 

280 if self.lsstLogLevel == -1: 

281 self.lsstLogLevel = CliLog.defaultLsstLogLevel 

282 

283 def __repr__(self): 

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

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

286 

287 @classmethod 

288 def _recordComponentSetting(cls, component): 

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

290 component levels.""" 

291 componentSettings = cls.ComponentSettings(component) 

292 cls._componentSettings.append(componentSettings) 

293 

294 @classmethod 

295 def replayConfigState(cls, configState): 

296 """Re-create configuration using configuration state recorded earlier. 

297 

298 Parameters 

299 ---------- 

300 configState : `list` of `tuple` 

301 Tuples contain a method as first item and arguments for the method, 

302 in the same format as ``cls.configState``. 

303 """ 

304 if cls._initialized or cls.configState: 

305 # Already initialized, do not touch anything. 

306 log = logging.getLogger(__name__) 

307 log.warning("Log is already initialized, will not replay configuration.") 

308 return 

309 

310 # execute each one in order 

311 for call in configState: 

312 method, *args = call 

313 method(*args)