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

124 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-09 02:45 -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 

30from deprecated.sphinx import deprecated 

31 

32try: 

33 import lsst.log.utils as logUtils 

34except ImportError: 

35 logUtils = None 

36 

37# log level for trace (verbose debug). 

38TRACE = 5 

39logging.addLevelName(TRACE, "TRACE") 

40 

41# Verbose logging is midway between INFO and DEBUG. 

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

43logging.addLevelName(VERBOSE, "VERBOSE") 

44 

45 

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

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

48 less than or equal to the provided value. 

49 

50 Parameters 

51 ---------- 

52 name : `str` 

53 Name of the logger. 

54 number : `int` 

55 The trace number threshold for display. 

56 

57 Examples 

58 -------- 

59 .. code-block:: python 

60 

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

62 

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

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

65 

66 Notes 

67 ----- 

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

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

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

71 issue ``DEBUG`` log messages. 

72 

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

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

75 also configured correctly. 

76 """ 

77 for i in range(6): 

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

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

80 

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

82 if logUtils is not None: 

83 logUtils.traceSetAt(name, number) 

84 

85 

86class _F: 

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

88 

89 Notes 

90 ----- 

91 This follows the recommendation from 

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

93 """ 

94 

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

96 self.fmt = fmt 

97 self.args = args 

98 self.kwargs = kwargs 

99 

100 def __str__(self) -> str: 

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

102 

103 

104class LsstLogAdapter(LoggerAdapter): 

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

106 

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

108 

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

110 

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

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

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

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

115 know the underlying logger class. 

116 """ 

117 

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

119 # something supported by Python loggers but can simplify some 

120 # logic if the logger is available. 

121 CRITICAL = logging.CRITICAL 

122 ERROR = logging.ERROR 

123 DEBUG = logging.DEBUG 

124 INFO = logging.INFO 

125 WARNING = logging.WARNING 

126 

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

128 FATAL = logging.FATAL 

129 WARN = logging.WARN 

130 

131 # These are specific to Tasks 

132 TRACE = TRACE 

133 VERBOSE = VERBOSE 

134 

135 @contextmanager 

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

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

138 

139 Parameters 

140 ---------- 

141 level : `int` 

142 The new temporary log level. 

143 """ 

144 old = self.level 

145 self.setLevel(level) 

146 try: 

147 yield 

148 finally: 

149 self.setLevel(old) 

150 

151 @property 

152 def level(self) -> int: 

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

154 return self.logger.level 

155 

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

157 """Get the named child logger. 

158 

159 Parameters 

160 ---------- 

161 name : `str` 

162 Name of the child relative to this logger. 

163 

164 Returns 

165 ------- 

166 child : `LsstLogAdapter` 

167 The child logger. 

168 """ 

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

170 

171 @deprecated( 

172 reason="Use Python Logger compatible isEnabledFor Will be removed after v23.", 

173 version="v23", 

174 category=FutureWarning, 

175 ) 

176 def isDebugEnabled(self) -> bool: 

177 return self.isEnabledFor(self.DEBUG) 

178 

179 @deprecated( 

180 reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.", 

181 version="v23", 

182 category=FutureWarning, 

183 ) 

184 def getName(self) -> str: 

185 return self.name 

186 

187 @deprecated( 

188 reason="Use Python Logger compatible .level property. Will be removed after v23.", 

189 version="v23", 

190 category=FutureWarning, 

191 ) 

192 def getLevel(self) -> int: 

193 return self.logger.level 

194 

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

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

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

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

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

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

201 

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

203 """Issue a VERBOSE level log message. 

204 

205 Arguments are as for `logging.info`. 

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

207 """ 

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

209 # method. 

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

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

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

213 # itself. 

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

215 

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

217 """Issue a TRACE level log message. 

218 

219 Arguments are as for `logging.info`. 

220 ``TRACE`` is lower than ``DEBUG``. 

221 """ 

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

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

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

225 

226 @deprecated( 

227 reason="Use Python Logger compatible method. Will be removed after v23.", 

228 version="v23", 

229 category=FutureWarning, 

230 ) 

231 def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

232 # Stacklevel is 4 to account for the deprecation wrapper 

233 self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4) 

234 

235 @deprecated( 

236 reason="Use Python Logger compatible method. Will be removed after v23.", 

237 version="v23", 

238 category=FutureWarning, 

239 ) 

240 def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

241 self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4) 

242 

243 @deprecated( 

244 reason="Use Python Logger compatible method. Will be removed after v23.", 

245 version="v23", 

246 category=FutureWarning, 

247 ) 

248 def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

249 self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4) 

250 

251 @deprecated( 

252 reason="Use Python Logger compatible method. Will be removed after v23.", 

253 version="v23", 

254 category=FutureWarning, 

255 ) 

256 def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

257 self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4) 

258 

259 @deprecated( 

260 reason="Use Python Logger compatible method. Will be removed after v23.", 

261 version="v23", 

262 category=FutureWarning, 

263 ) 

264 def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

265 self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4) 

266 

267 @deprecated( 

268 reason="Use Python Logger compatible method. Will be removed after v23.", 

269 version="v23", 

270 category=FutureWarning, 

271 ) 

272 def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

273 self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4) 

274 

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

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

277 

278 Parameters 

279 ---------- 

280 level : `int` 

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

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

283 """ 

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

285 self.logger.warning( 

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

287 level, 

288 ) 

289 level //= 1000 

290 

291 self.logger.setLevel(level) 

292 

293 @property 

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

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

296 return self.logger.handlers 

297 

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

299 """Add a handler to this logger. 

300 

301 The handler is forwarded to the underlying logger. 

302 """ 

303 self.logger.addHandler(handler) 

304 

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

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

307 self.logger.removeHandler(handler) 

308 

309 

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

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

312 

313 Parameters 

314 ---------- 

315 name : `str`, optional 

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

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

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

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

320 supplied logger. 

321 

322 Returns 

323 ------- 

324 logger : `LsstLogAdapter` 

325 The relevant logger. 

326 

327 Notes 

328 ----- 

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

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

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

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

333 used before the `Task` was created. 

334 """ 

335 if not logger: 

336 logger = logging.getLogger(name) 

337 elif name: 

338 logger = logger.getChild(name) 

339 return LsstLogAdapter(logger, {}) 

340 

341 

342LsstLoggers = Union[logging.Logger, LsstLogAdapter] 

343 

344 

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

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

347 

348 Parameters 

349 ---------- 

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

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

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

353 trace_level : `int` 

354 The trace level to use for the logger. 

355 

356 Returns 

357 ------- 

358 trace_logger : `LsstLogAdapter` 

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

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

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

362 """ 

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

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

365 return getLogger(trace_name) 

366 

367 

368class PeriodicLogger: 

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

370 

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

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

373 algorithm is progressing. 

374 

375 Parameters 

376 ---------- 

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

378 Logger to use when issuing a message. 

379 interval : `float` 

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

381 default will be used. 

382 level : `int`, optional 

383 Log level to use when issuing messages. 

384 """ 

385 

386 LOGGING_INTERVAL = 600.0 

387 """Default interval between log messages.""" 

388 

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

390 self.logger = logger 

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

392 self.level = level 

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

394 self.num_issued = 0 

395 

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

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

398 # level of indirection. 

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

400 

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

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

403 

404 Parameters 

405 ---------- 

406 msg : `str` 

407 Message to issue if the time has been exceeded. 

408 *args : Any 

409 Parameters to be passed to the log system. 

410 

411 Returns 

412 ------- 

413 issued : `bool` 

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

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

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

417 issued by the logging system. 

418 """ 

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

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

421 self.next_log_time = current_time + self.interval 

422 self.num_issued += 1 

423 return True 

424 return False