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

Shortcuts 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

120 statements  

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__ = ("TRACE", "VERBOSE", "getLogger", "LsstLogAdapter", "PeriodicLogger", "trace_set_at") 

15 

16import logging 

17import time 

18from contextlib import contextmanager 

19from logging import LoggerAdapter 

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

21 

22from deprecated.sphinx import deprecated 

23 

24try: 

25 import lsst.log.utils as logUtils 

26except ImportError: 

27 logUtils = None 

28 

29# log level for trace (verbose debug). 

30TRACE = 5 

31logging.addLevelName(TRACE, "TRACE") 

32 

33# Verbose logging is midway between INFO and DEBUG. 

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

35logging.addLevelName(VERBOSE, "VERBOSE") 

36 

37 

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

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

40 less than or equal to the provided value. 

41 

42 Parameters 

43 ---------- 

44 name : `str` 

45 Name of the logger. 

46 number : `int` 

47 The trace number threshold for display. 

48 

49 Examples 

50 -------- 

51 .. code-block:: python 

52 

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

54 

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

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

57 

58 Notes 

59 ----- 

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

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

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

63 issue ``DEBUG`` log messages. 

64 

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

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

67 also configured correctly. 

68 """ 

69 for i in range(6): 

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

71 log_name = f"TRACE{i}.{name}" if name else f"TRACE{i}" 

72 logging.getLogger(log_name).setLevel(level) 

73 

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

75 if logUtils is not None: 

76 logUtils.traceSetAt(name, number) 

77 

78 

79class _F: 

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

81 

82 Notes 

83 ----- 

84 This follows the recommendation from 

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

86 """ 

87 

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

89 self.fmt = fmt 

90 self.args = args 

91 self.kwargs = kwargs 

92 

93 def __str__(self) -> str: 

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

95 

96 

97class LsstLogAdapter(LoggerAdapter): 

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

99 

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

101 

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

103 

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

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

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

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

108 know the underlying logger class. 

109 """ 

110 

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

112 # something supported by Python loggers but can simplify some 

113 # logic if the logger is available. 

114 CRITICAL = logging.CRITICAL 

115 ERROR = logging.ERROR 

116 DEBUG = logging.DEBUG 

117 INFO = logging.INFO 

118 WARNING = logging.WARNING 

119 

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

121 FATAL = logging.FATAL 

122 WARN = logging.WARN 

123 

124 # These are specific to Tasks 

125 TRACE = TRACE 

126 VERBOSE = VERBOSE 

127 

128 @contextmanager 

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

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

131 

132 Parameters 

133 ---------- 

134 level : `int` 

135 The new temporary log level. 

136 """ 

137 old = self.level 

138 self.setLevel(level) 

139 try: 

140 yield 

141 finally: 

142 self.setLevel(old) 

143 

144 @property 

145 def level(self) -> int: 

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

147 return self.logger.level 

148 

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

150 """Get the named child logger. 

151 

152 Parameters 

153 ---------- 

154 name : `str` 

155 Name of the child relative to this logger. 

156 

157 Returns 

158 ------- 

159 child : `LsstLogAdapter` 

160 The child logger. 

161 """ 

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

163 

164 @deprecated( 

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

166 version="v23", 

167 category=FutureWarning, 

168 ) 

169 def isDebugEnabled(self) -> bool: 

170 return self.isEnabledFor(self.DEBUG) 

171 

172 @deprecated( 

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

174 version="v23", 

175 category=FutureWarning, 

176 ) 

177 def getName(self) -> str: 

178 return self.name 

179 

180 @deprecated( 

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

182 version="v23", 

183 category=FutureWarning, 

184 ) 

185 def getLevel(self) -> int: 

186 return self.logger.level 

187 

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

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

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

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

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

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

194 

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

196 """Issue a VERBOSE level log message. 

197 

198 Arguments are as for `logging.info`. 

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

200 """ 

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

202 # method. 

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

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

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

206 # itself. 

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

208 

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

210 """Issue a TRACE level log message. 

211 

212 Arguments are as for `logging.info`. 

213 ``TRACE`` is lower than ``DEBUG``. 

214 """ 

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

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

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

218 

219 @deprecated( 

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

221 version="v23", 

222 category=FutureWarning, 

223 ) 

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

225 # Stacklevel is 4 to account for the deprecation wrapper 

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

227 

228 @deprecated( 

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

230 version="v23", 

231 category=FutureWarning, 

232 ) 

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

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

235 

236 @deprecated( 

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

238 version="v23", 

239 category=FutureWarning, 

240 ) 

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

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

243 

244 @deprecated( 

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

246 version="v23", 

247 category=FutureWarning, 

248 ) 

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

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

251 

252 @deprecated( 

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

254 version="v23", 

255 category=FutureWarning, 

256 ) 

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

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

259 

260 @deprecated( 

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

262 version="v23", 

263 category=FutureWarning, 

264 ) 

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

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

267 

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

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

270 

271 Parameters 

272 ---------- 

273 level : `int` 

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

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

276 """ 

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

278 self.logger.warning( 

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

280 level, 

281 ) 

282 level //= 1000 

283 

284 self.logger.setLevel(level) 

285 

286 @property 

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

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

289 return self.logger.handlers 

290 

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

292 """Add a handler to this logger. 

293 

294 The handler is forwarded to the underlying logger. 

295 """ 

296 self.logger.addHandler(handler) 

297 

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

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

300 self.logger.removeHandler(handler) 

301 

302 

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

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

305 

306 Parameters 

307 ---------- 

308 name : `str`, optional 

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

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

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

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

313 supplied logger. 

314 

315 Returns 

316 ------- 

317 logger : `LsstLogAdapter` 

318 The relevant logger. 

319 

320 Notes 

321 ----- 

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

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

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

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

326 used before the `Task` was created. 

327 """ 

328 if not logger: 

329 logger = logging.getLogger(name) 

330 elif name: 

331 logger = logger.getChild(name) 

332 return LsstLogAdapter(logger, {}) 

333 

334 

335LsstLoggers = Union[logging.Logger, LsstLogAdapter] 

336 

337 

338class PeriodicLogger: 

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

340 

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

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

343 algorithm is progressing. 

344 

345 Parameters 

346 ---------- 

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

348 Logger to use when issuing a message. 

349 interval : `float` 

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

351 default will be used. 

352 level : `int`, optional 

353 Log level to use when issuing messages. 

354 """ 

355 

356 LOGGING_INTERVAL = 600.0 

357 """Default interval between log messages.""" 

358 

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

360 self.logger = logger 

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

362 self.level = level 

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

364 self.num_issued = 0 

365 

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

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

368 # level of indirection. 

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

370 

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

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

373 

374 Parameters 

375 ---------- 

376 msg : `str` 

377 Message to issue if the time has been exceeded. 

378 *args : Any 

379 Parameters to be passed to the log system. 

380 

381 Returns 

382 ------- 

383 issued : `bool` 

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

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

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

387 issued by the logging system. 

388 """ 

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

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

391 self.next_log_time = current_time + self.interval 

392 self.num_issued += 1 

393 return True 

394 return False