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 logging import LoggerAdapter 

18from deprecated.sphinx import deprecated 

19from contextlib import contextmanager 

20 

21from typing import ( 

22 Any, 

23 Generator, 

24 List, 

25 Optional, 

26 Union, 

27) 

28 

29try: 

30 import lsst.log.utils as logUtils 

31except ImportError: 

32 logUtils = None 

33 

34# log level for trace (verbose debug). 

35TRACE = 5 

36logging.addLevelName(TRACE, "TRACE") 

37 

38# Verbose logging is midway between INFO and DEBUG. 

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

40logging.addLevelName(VERBOSE, "VERBOSE") 

41 

42 

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

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

45 less than or equal to the provided value. 

46 

47 Parameters 

48 ---------- 

49 name : `str` 

50 Name of the logger. 

51 number : `int` 

52 The trace number threshold for display. 

53 

54 Examples 

55 -------- 

56 .. code-block:: python 

57 

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

59 

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

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

62 

63 Notes 

64 ----- 

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

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

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

68 issue ``DEBUG`` log messages. 

69 

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

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

72 also configured correctly. 

73 """ 

74 for i in range(6): 

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

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

77 logging.getLogger(log_name).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 @deprecated(reason="Use Python Logger compatible isEnabledFor Will be removed after v23.", 

170 version="v23", category=FutureWarning) 

171 def isDebugEnabled(self) -> bool: 

172 return self.isEnabledFor(self.DEBUG) 

173 

174 @deprecated(reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.", 

175 version="v23", category=FutureWarning) 

176 def getName(self) -> str: 

177 return self.name 

178 

179 @deprecated(reason="Use Python Logger compatible .level property. Will be removed after v23.", 

180 version="v23", category=FutureWarning) 

181 def getLevel(self) -> int: 

182 return self.logger.level 

183 

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

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

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

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

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

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

190 

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

192 """Issue a VERBOSE level log message. 

193 

194 Arguments are as for `logging.info`. 

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

196 """ 

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

198 # method. 

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

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

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

202 # itself. 

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

204 

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

206 """Issue a TRACE level log message. 

207 

208 Arguments are as for `logging.info`. 

209 ``TRACE`` is lower than ``DEBUG``. 

210 """ 

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

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

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

214 

215 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.", 

216 version="v23", category=FutureWarning) 

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

218 # Stacklevel is 4 to account for the deprecation wrapper 

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

220 

221 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.", 

222 version="v23", category=FutureWarning) 

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

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

225 

226 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.", 

227 version="v23", category=FutureWarning) 

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

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

230 

231 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.", 

232 version="v23", category=FutureWarning) 

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

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

235 

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

237 version="v23", category=FutureWarning) 

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

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

240 

241 @deprecated(reason="Use Python Logger compatible method. Will be removed after v23.", 

242 version="v23", category=FutureWarning) 

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

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

245 

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

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

248 

249 Parameters 

250 ---------- 

251 level : `int` 

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

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

254 """ 

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

256 self.logger.warning("Attempting to set level to %d -- looks like an lsst.log level so scaling it" 

257 " accordingly.", level) 

258 level //= 1000 

259 

260 self.logger.setLevel(level) 

261 

262 @property 

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

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

265 return self.logger.handlers 

266 

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

268 """Add a handler to this logger. 

269 

270 The handler is forwarded to the underlying logger. 

271 """ 

272 self.logger.addHandler(handler) 

273 

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

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

276 self.logger.removeHandler(handler) 

277 

278 

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

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

281 

282 Parameters 

283 ---------- 

284 name : `str`, optional 

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

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

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

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

289 supplied logger. 

290 

291 Returns 

292 ------- 

293 logger : `LsstLogAdapter` 

294 The relevant logger. 

295 

296 Notes 

297 ----- 

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

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

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

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

302 used before the `Task` was created. 

303 """ 

304 if not logger: 

305 logger = logging.getLogger(name) 

306 elif name: 

307 logger = logger.getChild(name) 

308 return LsstLogAdapter(logger, {}) 

309 

310 

311LsstLoggers = Union[logging.Logger, LsstLogAdapter]