Coverage for python/lsst/utils/logging.py: 46%

96 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-14 01:59 -0800

1# This file is part of utils. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ( 

15 "TRACE", 

16 "VERBOSE", 

17 "getLogger", 

18 "getTraceLogger", 

19 "LsstLogAdapter", 

20 "PeriodicLogger", 

21 "trace_set_at", 

22) 

23 

24import logging 

25import time 

26from contextlib import contextmanager 

27from logging import LoggerAdapter 

28from typing import Any, Generator, List, Optional, Union 

29 

30try: 

31 import lsst.log.utils as logUtils 

32except ImportError: 

33 logUtils = None 

34 

35# log level for trace (verbose debug). 

36TRACE = 5 

37logging.addLevelName(TRACE, "TRACE") 

38 

39# Verbose logging is midway between INFO and DEBUG. 

40VERBOSE = (logging.INFO + logging.DEBUG) // 2 

41logging.addLevelName(VERBOSE, "VERBOSE") 

42 

43 

44def trace_set_at(name: str, number: int) -> None: 

45 """Adjust logging level to display messages with the trace number being 

46 less than or equal to the provided value. 

47 

48 Parameters 

49 ---------- 

50 name : `str` 

51 Name of the logger. 

52 number : `int` 

53 The trace number threshold for display. 

54 

55 Examples 

56 -------- 

57 .. code-block:: python 

58 

59 lsst.utils.logging.trace_set_at("lsst.afw", 3) 

60 

61 This will set loggers ``TRACE0.lsst.afw`` to ``TRACE3.lsst.afw`` to 

62 ``DEBUG`` and ``TRACE4.lsst.afw`` and ``TRACE5.lsst.afw`` to ``INFO``. 

63 

64 Notes 

65 ----- 

66 Loggers ``TRACE0.`` to ``TRACE5.`` are set. All loggers above 

67 the specified threshold are set to ``INFO`` and those below the threshold 

68 are set to ``DEBUG``. The expectation is that ``TRACE`` loggers only 

69 issue ``DEBUG`` log messages. 

70 

71 If ``lsst.log`` is installed, this function will also call 

72 `lsst.log.utils.traceSetAt` to ensure that non-Python loggers are 

73 also configured correctly. 

74 """ 

75 for i in range(6): 

76 level = logging.INFO if i > number else logging.DEBUG 

77 getTraceLogger(name, i).setLevel(level) 

78 

79 # if lsst log is available also set the trace loggers there. 

80 if logUtils is not None: 

81 logUtils.traceSetAt(name, number) 

82 

83 

84class _F: 

85 """Format, supporting `str.format()` syntax. 

86 

87 Notes 

88 ----- 

89 This follows the recommendation from 

90 https://docs.python.org/3/howto/logging-cookbook.html#using-custom-message-objects 

91 """ 

92 

93 def __init__(self, fmt: str, /, *args: Any, **kwargs: Any): 

94 self.fmt = fmt 

95 self.args = args 

96 self.kwargs = kwargs 

97 

98 def __str__(self) -> str: 

99 return self.fmt.format(*self.args, **self.kwargs) 

100 

101 

102class LsstLogAdapter(LoggerAdapter): 

103 """A special logging adapter to provide log features for LSST code. 

104 

105 Expected to be instantiated initially by a call to `getLogger()`. 

106 

107 This class provides enhancements over `logging.Logger` that include: 

108 

109 * Methods for issuing trace and verbose level log messages. 

110 * Provision of a context manager to temporarily change the log level. 

111 * Attachment of logging level constants to the class to make it easier 

112 for a Task writer to access a specific log level without having to 

113 know the underlying logger class. 

114 """ 

115 

116 # Store logging constants in the class for convenience. This is not 

117 # something supported by Python loggers but can simplify some 

118 # logic if the logger is available. 

119 CRITICAL = logging.CRITICAL 

120 ERROR = logging.ERROR 

121 DEBUG = logging.DEBUG 

122 INFO = logging.INFO 

123 WARNING = logging.WARNING 

124 

125 # Python supports these but prefers they are not used. 

126 FATAL = logging.FATAL 

127 WARN = logging.WARN 

128 

129 # These are specific to Tasks 

130 TRACE = TRACE 

131 VERBOSE = VERBOSE 

132 

133 @contextmanager 

134 def temporary_log_level(self, level: Union[int, str]) -> Generator: 

135 """Temporarily set the level of this logger. 

136 

137 Parameters 

138 ---------- 

139 level : `int` 

140 The new temporary log level. 

141 """ 

142 old = self.level 

143 self.setLevel(level) 

144 try: 

145 yield 

146 finally: 

147 self.setLevel(old) 

148 

149 @property 

150 def level(self) -> int: 

151 """Return current level of this logger (``int``).""" 

152 return self.logger.level 

153 

154 def getChild(self, name: str) -> LsstLogAdapter: 

155 """Get the named child logger. 

156 

157 Parameters 

158 ---------- 

159 name : `str` 

160 Name of the child relative to this logger. 

161 

162 Returns 

163 ------- 

164 child : `LsstLogAdapter` 

165 The child logger. 

166 """ 

167 return getLogger(name=name, logger=self.logger) 

168 

169 def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None: 

170 # Python does not provide this method in LoggerAdapter but does 

171 # not formally deprecated it in favor of critical() either. 

172 # Provide it without deprecation message for consistency with Python. 

173 # stacklevel=5 accounts for the forwarding of LoggerAdapter. 

174 self.critical(msg, *args, **kwargs, stacklevel=4) 

175 

176 def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

177 """Issue a VERBOSE level log message. 

178 

179 Arguments are as for `logging.info`. 

180 ``VERBOSE`` is between ``DEBUG`` and ``INFO``. 

181 """ 

182 # There is no other way to achieve this other than a special logger 

183 # method. 

184 # Stacklevel is passed in so that the correct line is reported 

185 # in the log record and not this line. 3 is this method, 

186 # 2 is the level from `self.log` and 1 is the log infrastructure 

187 # itself. 

188 self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs) 

189 

190 def trace(self, fmt: str, *args: Any) -> None: 

191 """Issue a TRACE level log message. 

192 

193 Arguments are as for `logging.info`. 

194 ``TRACE`` is lower than ``DEBUG``. 

195 """ 

196 # There is no other way to achieve this other than a special logger 

197 # method. For stacklevel discussion see `verbose()`. 

198 self.log(TRACE, fmt, *args, stacklevel=3) 

199 

200 def setLevel(self, level: Union[int, str]) -> None: 

201 """Set the level for the logger, trapping lsst.log values. 

202 

203 Parameters 

204 ---------- 

205 level : `int` 

206 The level to use. If the level looks too big to be a Python 

207 logging level it is assumed to be a lsst.log level. 

208 """ 

209 if isinstance(level, int) and level > logging.CRITICAL: 

210 self.logger.warning( 

211 "Attempting to set level to %d -- looks like an lsst.log level so scaling it accordingly.", 

212 level, 

213 ) 

214 level //= 1000 

215 

216 self.logger.setLevel(level) 

217 

218 @property 

219 def handlers(self) -> List[logging.Handler]: 

220 """Log handlers associated with this logger.""" 

221 return self.logger.handlers 

222 

223 def addHandler(self, handler: logging.Handler) -> None: 

224 """Add a handler to this logger. 

225 

226 The handler is forwarded to the underlying logger. 

227 """ 

228 self.logger.addHandler(handler) 

229 

230 def removeHandler(self, handler: logging.Handler) -> None: 

231 """Remove the given handler from the underlying logger.""" 

232 self.logger.removeHandler(handler) 

233 

234 

235def getLogger(name: Optional[str] = None, logger: Optional[logging.Logger] = None) -> LsstLogAdapter: 

236 """Get a logger compatible with LSST usage. 

237 

238 Parameters 

239 ---------- 

240 name : `str`, optional 

241 Name of the logger. Root logger if `None`. 

242 logger : `logging.Logger` or `LsstLogAdapter` 

243 If given the logger is converted to the relevant logger class. 

244 If ``name`` is given the logger is assumed to be a child of the 

245 supplied logger. 

246 

247 Returns 

248 ------- 

249 logger : `LsstLogAdapter` 

250 The relevant logger. 

251 

252 Notes 

253 ----- 

254 A `logging.LoggerAdapter` is used since it is easier to provide a more 

255 uniform interface than when using `logging.setLoggerClass`. An adapter 

256 can be wrapped around the root logger and the `~logging.setLoggerClass` 

257 will return the logger first given that name even if the name was 

258 used before the `Task` was created. 

259 """ 

260 if not logger: 

261 logger = logging.getLogger(name) 

262 elif name: 

263 logger = logger.getChild(name) 

264 return LsstLogAdapter(logger, {}) 

265 

266 

267LsstLoggers = Union[logging.Logger, LsstLogAdapter] 

268 

269 

270def getTraceLogger(logger: Union[str, LsstLoggers], trace_level: int) -> LsstLogAdapter: 

271 """Get a logger with the appropriate TRACE name. 

272 

273 Parameters 

274 ---------- 

275 logger : `logging.Logger` or `LsstLogAdapter` or `lsst.log.Log` or `str` 

276 A logger to be used to derive the new trace logger. Can be a logger 

277 object that supports the ``name`` property or a string. 

278 trace_level : `int` 

279 The trace level to use for the logger. 

280 

281 Returns 

282 ------- 

283 trace_logger : `LsstLogAdapter` 

284 A new trace logger. The name will be derived by pre-pending ``TRACEn.`` 

285 to the name of the supplied logger. If the root logger is given 

286 the returned logger will be named ``TRACEn``. 

287 """ 

288 name = getattr(logger, "name", str(logger)) 

289 trace_name = f"TRACE{trace_level}.{name}" if name else f"TRACE{trace_level}" 

290 return getLogger(trace_name) 

291 

292 

293class PeriodicLogger: 

294 """Issue log messages if a time threshold has elapsed. 

295 

296 This class can be used in long-running sections of code where it would 

297 be useful to issue a log message periodically to show that the 

298 algorithm is progressing. 

299 

300 Parameters 

301 ---------- 

302 logger : `logging.Logger` or `LsstLogAdapter` 

303 Logger to use when issuing a message. 

304 interval : `float` 

305 The minimum interval between log messages. If `None` the class 

306 default will be used. 

307 level : `int`, optional 

308 Log level to use when issuing messages. 

309 """ 

310 

311 LOGGING_INTERVAL = 600.0 

312 """Default interval between log messages.""" 

313 

314 def __init__(self, logger: LsstLoggers, interval: Optional[float] = None, level: int = VERBOSE): 

315 self.logger = logger 

316 self.interval = interval if interval is not None else self.LOGGING_INTERVAL 

317 self.level = level 

318 self.next_log_time = time.time() + self.interval 

319 self.num_issued = 0 

320 

321 # The stacklevel we need to issue logs is determined by the type 

322 # of logger we have been given. A LoggerAdapter has an extra 

323 # level of indirection. 

324 self._stacklevel = 3 if isinstance(self.logger, LoggerAdapter) else 2 

325 

326 def log(self, msg: str, *args: Any) -> bool: 

327 """Issue a log message if the interval has elapsed. 

328 

329 Parameters 

330 ---------- 

331 msg : `str` 

332 Message to issue if the time has been exceeded. 

333 *args : Any 

334 Parameters to be passed to the log system. 

335 

336 Returns 

337 ------- 

338 issued : `bool` 

339 Returns `True` if a log message was sent to the logging system. 

340 Returns `False` if the interval has not yet elapsed. Returning 

341 `True` does not indicate whether the log message was in fact 

342 issued by the logging system. 

343 """ 

344 if (current_time := time.time()) > self.next_log_time: 

345 self.logger.log(self.level, msg, *args, stacklevel=self._stacklevel) 

346 self.next_log_time = current_time + self.interval 

347 self.num_issued += 1 

348 return True 

349 return False