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

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

103 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", "trace_set_at") 

15 

16import logging 

17from contextlib import contextmanager 

18from logging import LoggerAdapter 

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

20 

21from deprecated.sphinx import deprecated 

22 

23try: 

24 import lsst.log.utils as logUtils 

25except ImportError: 

26 logUtils = None 

27 

28# log level for trace (verbose debug). 

29TRACE = 5 

30logging.addLevelName(TRACE, "TRACE") 

31 

32# Verbose logging is midway between INFO and DEBUG. 

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

34logging.addLevelName(VERBOSE, "VERBOSE") 

35 

36 

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

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

39 less than or equal to the provided value. 

40 

41 Parameters 

42 ---------- 

43 name : `str` 

44 Name of the logger. 

45 number : `int` 

46 The trace number threshold for display. 

47 

48 Examples 

49 -------- 

50 .. code-block:: python 

51 

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

53 

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

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

56 

57 Notes 

58 ----- 

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

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

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

62 issue ``DEBUG`` log messages. 

63 

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

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

66 also configured correctly. 

67 """ 

68 for i in range(6): 

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

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

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

72 

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

74 if logUtils is not None: 

75 logUtils.traceSetAt(name, number) 

76 

77 

78class _F: 

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

80 

81 Notes 

82 ----- 

83 This follows the recommendation from 

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

85 """ 

86 

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

88 self.fmt = fmt 

89 self.args = args 

90 self.kwargs = kwargs 

91 

92 def __str__(self) -> str: 

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

94 

95 

96class LsstLogAdapter(LoggerAdapter): 

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

98 

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

100 

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

102 

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

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

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

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

107 know the underlying logger class. 

108 """ 

109 

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

111 # something supported by Python loggers but can simplify some 

112 # logic if the logger is available. 

113 CRITICAL = logging.CRITICAL 

114 ERROR = logging.ERROR 

115 DEBUG = logging.DEBUG 

116 INFO = logging.INFO 

117 WARNING = logging.WARNING 

118 

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

120 FATAL = logging.FATAL 

121 WARN = logging.WARN 

122 

123 # These are specific to Tasks 

124 TRACE = TRACE 

125 VERBOSE = VERBOSE 

126 

127 @contextmanager 

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

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

130 

131 Parameters 

132 ---------- 

133 level : `int` 

134 The new temporary log level. 

135 """ 

136 old = self.level 

137 self.setLevel(level) 

138 try: 

139 yield 

140 finally: 

141 self.setLevel(old) 

142 

143 @property 

144 def level(self) -> int: 

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

146 return self.logger.level 

147 

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

149 """Get the named child logger. 

150 

151 Parameters 

152 ---------- 

153 name : `str` 

154 Name of the child relative to this logger. 

155 

156 Returns 

157 ------- 

158 child : `LsstLogAdapter` 

159 The child logger. 

160 """ 

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

162 

163 @deprecated( 

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

165 version="v23", 

166 category=FutureWarning, 

167 ) 

168 def isDebugEnabled(self) -> bool: 

169 return self.isEnabledFor(self.DEBUG) 

170 

171 @deprecated( 

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

173 version="v23", 

174 category=FutureWarning, 

175 ) 

176 def getName(self) -> str: 

177 return self.name 

178 

179 @deprecated( 

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

181 version="v23", 

182 category=FutureWarning, 

183 ) 

184 def getLevel(self) -> int: 

185 return self.logger.level 

186 

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

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

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

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

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

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

193 

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

195 """Issue a VERBOSE level log message. 

196 

197 Arguments are as for `logging.info`. 

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

199 """ 

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

201 # method. 

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

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

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

205 # itself. 

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

207 

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

209 """Issue a TRACE level log message. 

210 

211 Arguments are as for `logging.info`. 

212 ``TRACE`` is lower than ``DEBUG``. 

213 """ 

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

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

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

217 

218 @deprecated( 

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

220 version="v23", 

221 category=FutureWarning, 

222 ) 

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

224 # Stacklevel is 4 to account for the deprecation wrapper 

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

226 

227 @deprecated( 

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

229 version="v23", 

230 category=FutureWarning, 

231 ) 

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

233 self.log(logging.DEBUG, _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 infof(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

241 self.log(logging.INFO, _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 warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

249 self.log(logging.WARNING, _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 errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

257 self.log(logging.ERROR, _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 fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None: 

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

266 

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

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

269 

270 Parameters 

271 ---------- 

272 level : `int` 

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

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

275 """ 

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

277 self.logger.warning( 

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

279 level, 

280 ) 

281 level //= 1000 

282 

283 self.logger.setLevel(level) 

284 

285 @property 

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

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

288 return self.logger.handlers 

289 

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

291 """Add a handler to this logger. 

292 

293 The handler is forwarded to the underlying logger. 

294 """ 

295 self.logger.addHandler(handler) 

296 

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

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

299 self.logger.removeHandler(handler) 

300 

301 

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

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

304 

305 Parameters 

306 ---------- 

307 name : `str`, optional 

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

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

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

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

312 supplied logger. 

313 

314 Returns 

315 ------- 

316 logger : `LsstLogAdapter` 

317 The relevant logger. 

318 

319 Notes 

320 ----- 

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

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

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

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

325 used before the `Task` was created. 

326 """ 

327 if not logger: 

328 logger = logging.getLogger(name) 

329 elif name: 

330 logger = logger.getChild(name) 

331 return LsstLogAdapter(logger, {}) 

332 

333 

334LsstLoggers = Union[logging.Logger, LsstLogAdapter]