Hide keyboard shortcuts

Hot-keys 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

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") 

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 

38class _F: 

39 """ 

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

41 

42 Notes 

43 ----- 

44 This follows the recommendation from 

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

46 """ 

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

48 self.fmt = fmt 

49 self.args = args 

50 self.kwargs = kwargs 

51 

52 def __str__(self) -> str: 

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

54 

55 

56class LsstLogAdapter(LoggerAdapter): 

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

58 

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

60 

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

62 

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

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

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

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

67 know the underlying logger class. 

68 """ 

69 

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

71 # something supported by Python loggers but can simplify some 

72 # logic if the logger is available. 

73 CRITICAL = logging.CRITICAL 

74 ERROR = logging.ERROR 

75 DEBUG = logging.DEBUG 

76 INFO = logging.INFO 

77 WARNING = logging.WARNING 

78 

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

80 FATAL = logging.FATAL 

81 WARN = logging.WARN 

82 

83 # These are specific to Tasks 

84 TRACE = TRACE 

85 VERBOSE = VERBOSE 

86 

87 @contextmanager 

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

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

90 

91 Parameters 

92 ---------- 

93 level : `int` 

94 The new temporary log level. 

95 """ 

96 old = self.level 

97 self.setLevel(level) 

98 try: 

99 yield 

100 finally: 

101 self.setLevel(old) 

102 

103 @property 

104 def level(self) -> int: 

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

106 return self.logger.level 

107 

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

109 """Get the named child logger. 

110 

111 Parameters 

112 ---------- 

113 name : `str` 

114 Name of the child relative to this logger. 

115 

116 Returns 

117 ------- 

118 child : `LsstLogAdapter` 

119 The child logger. 

120 """ 

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

122 

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

124 version="v23", category=FutureWarning) 

125 def isDebugEnabled(self) -> bool: 

126 return self.isEnabledFor(self.DEBUG) 

127 

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

129 version="v23", category=FutureWarning) 

130 def getName(self) -> str: 

131 return self.name 

132 

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

134 version="v23", category=FutureWarning) 

135 def getLevel(self) -> int: 

136 return self.logger.level 

137 

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

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

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

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

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

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

144 

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

146 """Issue a VERBOSE level log message. 

147 

148 Arguments are as for `logging.info`. 

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

150 """ 

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

152 # method. 

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

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

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

156 # itself. 

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

158 

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

160 """Issue a TRACE level log message. 

161 

162 Arguments are as for `logging.info`. 

163 ``TRACE`` is lower than ``DEBUG``. 

164 """ 

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

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

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

168 

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

170 version="v23", category=FutureWarning) 

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

172 # Stacklevel is 4 to account for the deprecation wrapper 

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

174 

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

176 version="v23", category=FutureWarning) 

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

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

179 

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

181 version="v23", category=FutureWarning) 

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

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

184 

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

186 version="v23", category=FutureWarning) 

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

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

189 

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

191 version="v23", category=FutureWarning) 

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

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

194 

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

196 version="v23", category=FutureWarning) 

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

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

199 

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

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

202 

203 Parameters 

204 ---------- 

205 level : `int` 

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

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

208 """ 

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

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

211 " accordingly.", level) 

212 level //= 1000 

213 

214 self.logger.setLevel(level) 

215 

216 @property 

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

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

219 return self.logger.handlers 

220 

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

222 """Add a handler to this logger. 

223 

224 The handler is forwarded to the underlying logger. 

225 """ 

226 self.logger.addHandler(handler) 

227 

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

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

230 self.logger.removeHandler(handler) 

231 

232 

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

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

235 

236 Parameters 

237 ---------- 

238 name : `str`, optional 

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

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

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

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

243 supplied logger. 

244 

245 Returns 

246 ------- 

247 logger : `LsstLogAdapter` 

248 The relevant logger. 

249 

250 Notes 

251 ----- 

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

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

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

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

256 used before the `Task` was created. 

257 """ 

258 if not logger: 

259 logger = logging.getLogger(name) 

260 elif name: 

261 logger = logger.getChild(name) 

262 return LsstLogAdapter(logger, {}) 

263 

264 

265LsstLoggers = Union[logging.Logger, LsstLogAdapter]