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

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

97 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 

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 """Adjusts 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 Notes 

50 ----- 

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

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

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

54 issue ``DEBUG`` log messages. 

55 

56 Examples 

57 -------- 

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 for i in range(6): 

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

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

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

70 

71 

72class _F: 

73 """ 

74 Format, supporting `str.format()` syntax. 

75 

76 Notes 

77 ----- 

78 This follows the recommendation from 

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

80 """ 

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

82 self.fmt = fmt 

83 self.args = args 

84 self.kwargs = kwargs 

85 

86 def __str__(self) -> str: 

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

88 

89 

90class LsstLogAdapter(LoggerAdapter): 

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

92 

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

94 

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

96 

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

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

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

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

101 know the underlying logger class. 

102 """ 

103 

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

105 # something supported by Python loggers but can simplify some 

106 # logic if the logger is available. 

107 CRITICAL = logging.CRITICAL 

108 ERROR = logging.ERROR 

109 DEBUG = logging.DEBUG 

110 INFO = logging.INFO 

111 WARNING = logging.WARNING 

112 

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

114 FATAL = logging.FATAL 

115 WARN = logging.WARN 

116 

117 # These are specific to Tasks 

118 TRACE = TRACE 

119 VERBOSE = VERBOSE 

120 

121 @contextmanager 

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

123 """A context manager that temporarily sets the level of this logger. 

124 

125 Parameters 

126 ---------- 

127 level : `int` 

128 The new temporary log level. 

129 """ 

130 old = self.level 

131 self.setLevel(level) 

132 try: 

133 yield 

134 finally: 

135 self.setLevel(old) 

136 

137 @property 

138 def level(self) -> int: 

139 """Current level of this logger (``int``).""" 

140 return self.logger.level 

141 

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

143 """Get the named child logger. 

144 

145 Parameters 

146 ---------- 

147 name : `str` 

148 Name of the child relative to this logger. 

149 

150 Returns 

151 ------- 

152 child : `LsstLogAdapter` 

153 The child logger. 

154 """ 

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

156 

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

158 version="v23", category=FutureWarning) 

159 def isDebugEnabled(self) -> bool: 

160 return self.isEnabledFor(self.DEBUG) 

161 

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

163 version="v23", category=FutureWarning) 

164 def getName(self) -> str: 

165 return self.name 

166 

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

168 version="v23", category=FutureWarning) 

169 def getLevel(self) -> int: 

170 return self.logger.level 

171 

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

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

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

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

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

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

178 

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

180 """Issue a VERBOSE level log message. 

181 

182 Arguments are as for `logging.info`. 

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

184 """ 

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

186 # method. 

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

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

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

190 # itself. 

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

192 

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

194 """Issue a TRACE level log message. 

195 

196 Arguments are as for `logging.info`. 

197 ``TRACE`` is lower than ``DEBUG``. 

198 """ 

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

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

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

202 

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

204 version="v23", category=FutureWarning) 

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

206 # Stacklevel is 4 to account for the deprecation wrapper 

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

208 

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

210 version="v23", category=FutureWarning) 

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

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

213 

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

215 version="v23", category=FutureWarning) 

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

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

218 

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

220 version="v23", category=FutureWarning) 

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

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

223 

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

225 version="v23", category=FutureWarning) 

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

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

228 

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

230 version="v23", category=FutureWarning) 

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

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

233 

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

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

236 

237 Parameters 

238 ---------- 

239 level : `int` 

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

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

242 """ 

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

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

245 " accordingly.", level) 

246 level //= 1000 

247 

248 self.logger.setLevel(level) 

249 

250 @property 

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

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

253 return self.logger.handlers 

254 

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

256 """Add a handler to this logger. 

257 

258 The handler is forwarded to the underlying logger. 

259 """ 

260 self.logger.addHandler(handler) 

261 

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

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

264 self.logger.removeHandler(handler) 

265 

266 

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

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

269 

270 Parameters 

271 ---------- 

272 name : `str`, optional 

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

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

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

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

277 supplied logger. 

278 

279 Returns 

280 ------- 

281 logger : `LsstLogAdapter` 

282 The relevant logger. 

283 

284 Notes 

285 ----- 

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

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

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

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

290 used before the `Task` was created. 

291 """ 

292 if not logger: 

293 logger = logging.getLogger(name) 

294 elif name: 

295 logger = logger.getChild(name) 

296 return LsstLogAdapter(logger, {}) 

297 

298 

299LsstLoggers = Union[logging.Logger, LsstLogAdapter]