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

43 initialization so that lsst.log is a handler for python logging. 

44 

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

48 

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

50 

51 longLogFmt = "%-5p %d{yyyy-MM-ddTHH:mm:ss.SSSZ} %c (%X{LABEL})(%F:%L)- %m%n" 

52 normalLogFmt = "%c %p: %m%n" 

53 

54 _initialized = False 

55 _lsstLogHandler = None 

56 _componentSettings = [] 

57 

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. 

62 

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

64 root logger's handlers. 

65 

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 

80 

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) 

96 

97 # also capture warnings and send them to logging 

98 logging.captureWarnings(True) 

99 

100 @classmethod 

101 def getHandlerId(cls): 

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

103 

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

105 handler. 

106 

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) 

113 

114 @classmethod 

115 def resetLog(cls): 

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

117 levels. 

118 

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

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

121 

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 

137 

138 @classmethod 

139 def setLogLevels(cls, logLevels): 

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

141 

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

152 

153 # configure individual loggers 

154 for component, level in logLevels: 

155 cls._setLogLevel(component, level) 

156 

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. 

162 

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

175 

176 @staticmethod 

177 def _getPyLogLevel(level): 

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

179 

180 Parameters 

181 ---------- 

182 level : `str` 

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

184 

185 Returns 

186 ------- 

187 numericValue : `int` 

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

189 """ 

190 return getattr(logging, level, None) 

191 

192 @staticmethod 

193 def _getLsstLogLevel(level): 

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

195 

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

200 

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

202 - CRITICAL to FATAL 

203 - WARNING to WARN 

204 

205 Parameters 

206 ---------- 

207 level : `str` 

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

209 

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) 

222 

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 

232 

233 def __repr__(self): 

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

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

236 

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)