Coverage for python/lsst/log/log/logContinued.py: 43%

190 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-05 01:21 +0000

1#!/usr/bin/env python 

2 

3# 

4# LSST Data Management System 

5# Copyright 2013 LSST Corporation. 

6# 

7# This product includes software developed by the 

8# LSST Project (http://www.lsst.org/). 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the LSST License Statement and 

21# the GNU General Public License along with this program. If not, 

22# see <http://www.lsstcorp.org/LegalNotices/>. 

23# 

24 

25__all__ = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "CRITICAL", "WARNING", 

26 "Log", "configure", "configure_prop", "configure_pylog_MDC", "getDefaultLogger", 

27 "getLogger", "MDC", "MDCDict", "MDCRemove", "MDCRegisterInit", "setLevel", 

28 "getLevel", "isEnabledFor", "log", "trace", "debug", "info", "warn", "warning", 

29 "error", "fatal", "critical", 

30 "lwpID", "usePythonLogging", "doNotUsePythonLogging", "UsePythonLogging", 

31 "LevelTranslator", "LogHandler", "getEffectiveLevel", "getLevelName"] 

32 

33import logging 

34import inspect 

35import os 

36 

37from typing import Optional 

38 

39from lsst.utils import continueClass 

40 

41from .log import Log 

42 

43TRACE = 5000 

44DEBUG = 10000 

45INFO = 20000 

46WARN = 30000 

47ERROR = 40000 

48FATAL = 50000 

49 

50# For compatibility with python logging 

51CRITICAL = FATAL 

52WARNING = WARN 

53 

54 

55@continueClass # noqa: F811 (FIXME: remove for py 3.8+) 

56class Log: # noqa: F811 

57 UsePythonLogging = False 

58 """Forward Python `lsst.log` messages to Python `logging` package.""" 

59 

60 CRITICAL = CRITICAL 

61 WARNING = WARNING 

62 

63 @classmethod 

64 def usePythonLogging(cls): 

65 """Forward log messages to Python `logging` 

66 

67 Notes 

68 ----- 

69 This is useful for unit testing when you want to ensure 

70 that log messages are captured by the testing environment 

71 as distinct from standard output. 

72 

73 This state only affects messages sent to the `lsst.log` 

74 package from Python. 

75 """ 

76 cls.UsePythonLogging = True 

77 

78 @classmethod 

79 def doNotUsePythonLogging(cls): 

80 """Forward log messages to LSST logging system. 

81 

82 Notes 

83 ----- 

84 This is the default state. 

85 """ 

86 cls.UsePythonLogging = False 

87 

88 @property 

89 def name(self): 

90 return self.getName() 

91 

92 @property 

93 def level(self): 

94 return self.getLevel() 

95 

96 @property 

97 def parent(self): 

98 """Returns the parent logger, or None if this is the root logger.""" 

99 if not self.name: 

100 return None 

101 parent_name = self.name.rpartition(".")[0] 

102 if not parent_name: 

103 return self.getDefaultLogger() 

104 return self.getLogger(parent_name) 

105 

106 def trace(self, fmt, *args): 

107 self._log(Log.TRACE, False, fmt, *args) 

108 

109 def debug(self, fmt, *args): 

110 self._log(Log.DEBUG, False, fmt, *args) 

111 

112 def info(self, fmt, *args): 

113 self._log(Log.INFO, False, fmt, *args) 

114 

115 def warn(self, fmt, *args): 

116 self._log(Log.WARN, False, fmt, *args) 

117 

118 def warning(self, fmt, *args): 

119 # Do not call warn() because that will result in an incorrect 

120 # line number in the log. 

121 self._log(Log.WARN, False, fmt, *args) 

122 

123 def error(self, fmt, *args): 

124 self._log(Log.ERROR, False, fmt, *args) 

125 

126 def fatal(self, fmt, *args): 

127 self._log(Log.FATAL, False, fmt, *args) 

128 

129 def critical(self, fmt, *args): 

130 # Do not call fatal() because that will result in an incorrect 

131 # line number in the log. 

132 self._log(Log.FATAL, False, fmt, *args) 

133 

134 def _log(self, level, use_format, fmt, *args, **kwargs): 

135 if self.isEnabledFor(level): 

136 frame = inspect.currentframe().f_back # calling method 

137 frame = frame.f_back # original log location 

138 filename = os.path.split(frame.f_code.co_filename)[1] 

139 funcname = frame.f_code.co_name 

140 if use_format: 

141 msg = fmt.format(*args, **kwargs) if args or kwargs else fmt 

142 else: 

143 msg = fmt % args if args else fmt 

144 if self.UsePythonLogging: 

145 levelno = LevelTranslator.lsstLog2logging(level) 

146 levelName = logging.getLevelName(levelno) 

147 

148 pylog = logging.getLogger(self.getName()) 

149 record = logging.makeLogRecord(dict(name=self.getName(), 

150 levelno=levelno, 

151 levelname=levelName, 

152 msg=msg, 

153 funcName=funcname, 

154 filename=filename, 

155 pathname=frame.f_code.co_filename, 

156 lineno=frame.f_lineno)) 

157 pylog.handle(record) 

158 else: 

159 self.logMsg(level, filename, funcname, frame.f_lineno, msg) 

160 

161 def __reduce__(self): 

162 """Implement pickle support. 

163 """ 

164 args = (self.getName(), ) 

165 # method has to be module-level, not class method 

166 return (getLogger, args) 

167 

168 def __repr__(self): 

169 # Match python logging style. 

170 cls = type(self) 

171 class_name = f"{cls.__module__}.{cls.__qualname__}" 

172 prefix = "lsst.log.log.log" 

173 if class_name.startswith(prefix): 

174 class_name = class_name.replace(prefix, "lsst.log") 

175 return f"<{class_name} '{self.name}' ({getLevelName(self.getEffectiveLevel())})>" 

176 

177 

178class MDCDict(dict): 

179 """Dictionary for MDC data. 

180 

181 This is internal class used for better formatting of MDC in Python logging 

182 output. It behaves like `defaultdict(str)` but overrides ``__str__`` and 

183 ``__repr__`` method to produce output better suited for logging records. 

184 """ 

185 def __getitem__(self, name: str): 

186 """Returns value for a given key or empty string for missing key. 

187 """ 

188 return self.get(name, "") 

189 

190 def __str__(self): 

191 """Return string representation, strings are interpolated without 

192 quotes. 

193 """ 

194 items = (f"{k}={self[k]}" for k in sorted(self)) 

195 return "{" + ", ".join(items) + "}" 

196 

197 def __repr__(self): 

198 return str(self) 

199 

200 

201# Export static functions from Log class to module namespace 

202 

203 

204def configure(*args): 

205 Log.configure(*args) 

206 

207 

208def configure_prop(properties): 

209 Log.configure_prop(properties) 

210 

211 

212def configure_pylog_MDC(level: str, MDC_class: Optional[type] = MDCDict): 

213 """Configure log4cxx to send messages to Python logging, with MDC support. 

214 

215 Parameters 

216 ---------- 

217 level : `str` 

218 Name of the logging level for root log4cxx logger. 

219 MDC_class : `type`, optional 

220 Type of dictionary which is added to `logging.LogRecord` as an ``MDC`` 

221 attribute. Any dictionary or ``defaultdict``-like class can be used as 

222 a type. If `None` the `logging.LogRecord` will not be augmented. 

223 

224 Notes 

225 ----- 

226 This method does two things: 

227 

228 - Configures log4cxx with a given logging level and a ``PyLogAppender`` 

229 appender class which forwards all messages to Python `logging`. 

230 - Installs a record factory for Python `logging` that adds ``MDC`` 

231 attribute to every `logging.LogRecord` object (instance of 

232 ``MDC_class``). This will happen by default but can be disabled 

233 by setting the ``MDC_class`` parameter to `None`. 

234 """ 

235 if MDC_class is not None: 

236 old_factory = logging.getLogRecordFactory() 

237 

238 def record_factory(*args, **kwargs): 

239 record = old_factory(*args, **kwargs) 

240 record.MDC = MDC_class() 

241 return record 

242 

243 logging.setLogRecordFactory(record_factory) 

244 

245 properties = """\ 

246log4j.rootLogger = {}, PyLog 

247log4j.appender.PyLog = PyLogAppender 

248""".format(level) 

249 configure_prop(properties) 

250 

251 

252def getDefaultLogger(): 

253 return Log.getDefaultLogger() 

254 

255 

256def getLogger(loggername): 

257 return Log.getLogger(loggername) 

258 

259 

260def MDC(key, value): 

261 return Log.MDC(key, str(value)) 

262 

263 

264def MDCRemove(key): 

265 Log.MDCRemove(key) 

266 

267 

268def MDCRegisterInit(func): 

269 Log.MDCRegisterInit(func) 

270 

271 

272def setLevel(loggername, level): 

273 Log.getLogger(loggername).setLevel(level) 

274 

275 

276def getLevel(loggername): 

277 return Log.getLogger(loggername).getLevel() 

278 

279 

280def getEffectiveLevel(loggername): 

281 return Log.getLogger(loggername).getEffectiveLevel() 

282 

283 

284def isEnabledFor(loggername, level): 

285 return Log.getLogger(loggername).isEnabledFor(level) 

286 

287 

288# This will cause a warning in Sphinx documentation due to confusion between 

289# Log and log. https://github.com/astropy/sphinx-automodapi/issues/73 (but 

290# note that this does not seem to be Mac-only). 

291def log(loggername, level, fmt, *args, **kwargs): 

292 Log.getLogger(loggername)._log(level, False, fmt, *args) 

293 

294 

295def trace(fmt, *args): 

296 Log.getDefaultLogger()._log(TRACE, False, fmt, *args) 

297 

298 

299def debug(fmt, *args): 

300 Log.getDefaultLogger()._log(DEBUG, False, fmt, *args) 

301 

302 

303def info(fmt, *args): 

304 Log.getDefaultLogger()._log(INFO, False, fmt, *args) 

305 

306 

307def warn(fmt, *args): 

308 Log.getDefaultLogger()._log(WARN, False, fmt, *args) 

309 

310 

311def warning(fmt, *args): 

312 warn(fmt, *args) 

313 

314 

315def error(fmt, *args): 

316 Log.getDefaultLogger()._log(ERROR, False, fmt, *args) 

317 

318 

319def fatal(fmt, *args): 

320 Log.getDefaultLogger()._log(FATAL, False, fmt, *args) 

321 

322 

323def critical(fmt, *args): 

324 fatal(fmt, *args) 

325 

326 

327def lwpID(): 

328 return Log.lwpID 

329 

330 

331def getLevelName(level): 

332 """Return the name associated with this logging level. 

333 

334 Returns "Level %d" if no name can be found. 

335 """ 

336 names = ("DEBUG", "TRACE", "WARNING", "FATAL", "INFO", "ERROR") 

337 for name in names: 

338 test_level = getattr(Log, name) 

339 if test_level == level: 

340 return name 

341 return f"Level {level}" 

342 

343 

344# This will cause a warning in Sphinx documentation due to confusion between 

345# UsePythonLogging and usePythonLogging. 

346# https://github.com/astropy/sphinx-automodapi/issues/73 (but note that this 

347# does not seem to be Mac-only). 

348def usePythonLogging(): 

349 Log.usePythonLogging() 

350 

351 

352def doNotUsePythonLogging(): 

353 Log.doNotUsePythonLogging() 

354 

355 

356class UsePythonLogging: 

357 """Context manager to enable Python log forwarding temporarily. 

358 """ 

359 

360 def __init__(self): 

361 self.current = Log.UsePythonLogging 

362 

363 def __enter__(self): 

364 Log.usePythonLogging() 

365 

366 def __exit__(self, exc_type, exc_value, traceback): 

367 Log.UsePythonLogging = self.current 

368 

369 

370class LevelTranslator: 

371 """Helper class to translate levels between ``lsst.log`` and Python 

372 `logging`. 

373 """ 

374 @staticmethod 

375 def lsstLog2logging(level): 

376 """Translates from lsst.log/log4cxx levels to `logging` module levels. 

377 

378 Parameters 

379 ---------- 

380 level : `int` 

381 Logging level number used by `lsst.log`, typically one of the 

382 constants defined in this module (`DEBUG`, `INFO`, etc.) 

383 

384 Returns 

385 ------- 

386 level : `int` 

387 Correspoding logging level number for Python `logging` module. 

388 """ 

389 # Python logging levels are same as lsst.log divided by 1000, 

390 # logging does not have TRACE level by default but it is OK to use 

391 # that numeric level and we may even add TRACE later. 

392 return level//1000 

393 

394 @staticmethod 

395 def logging2lsstLog(level): 

396 """Translates from standard python `logging` module levels to 

397 lsst.log/log4cxx levels. 

398 

399 Parameters 

400 ---------- 

401 level : `int` 

402 Logging level number used by Python `logging`, typically one of 

403 the constants defined by `logging` module (`logging.DEBUG`, 

404 `logging.INFO`, etc.) 

405 

406 Returns 

407 ------- 

408 level : `int` 

409 Correspoding logging level number for `lsst.log` module. 

410 """ 

411 return level*1000 

412 

413 

414class LogHandler(logging.Handler): 

415 """Handler for Python logging module that emits to LSST logging. 

416 

417 Parameters 

418 ---------- 

419 level : `int` 

420 Level at which to set the this handler. 

421 

422 Notes 

423 ----- 

424 If this handler is enabled and `lsst.log` has been configured to use 

425 Python `logging`, the handler will do nothing itself if any other 

426 handler has been registered with the Python logger. If it does not 

427 think that anything else is handling the message it will attempt to 

428 send the message via a default `~logging.StreamHandler`. The safest 

429 approach is to configure the logger with an additional handler 

430 (possibly the ROOT logger) if `lsst.log` is to be configured to use 

431 Python logging. 

432 """ 

433 

434 def __init__(self, level=logging.NOTSET): 

435 logging.Handler.__init__(self, level=level) 

436 # Format as a simple message because lsst.log will format the 

437 # message a second time. 

438 self.formatter = logging.Formatter(fmt="%(message)s") 

439 

440 def handle(self, record): 

441 logger = Log.getLogger(record.name) 

442 if logger.isEnabledFor(LevelTranslator.logging2lsstLog(record.levelno)): 

443 logging.Handler.handle(self, record) 

444 

445 def emit(self, record): 

446 if Log.UsePythonLogging: 

447 # Do not forward this message to lsst.log since this may cause 

448 # a logging loop. 

449 

450 # Work out whether any other handler is going to be invoked 

451 # for this logger. 

452 pylgr = logging.getLogger(record.name) 

453 

454 # If another handler is registered that is not LogHandler 

455 # we ignore this request 

456 if any(not isinstance(h, self.__class__) for h in pylgr.handlers): 

457 return 

458 

459 # If the parent has handlers and propagation is enabled 

460 # we punt as well (and if a LogHandler is involved then we will 

461 # ask the same question when we get to it). 

462 if pylgr.parent and pylgr.parent.hasHandlers() and pylgr.propagate: 

463 return 

464 

465 # Force this message to appear somewhere. 

466 # If something else should happen then the caller should add a 

467 # second Handler. 

468 stream = logging.StreamHandler() 

469 stream.setFormatter(logging.Formatter(fmt="%(name)s %(levelname)s (fallback): %(message)s")) 

470 stream.handle(record) 

471 return 

472 

473 logger = Log.getLogger(record.name) 

474 # Use standard formatting class to format message part of the record 

475 message = self.formatter.format(record) 

476 

477 logger.logMsg(LevelTranslator.logging2lsstLog(record.levelno), 

478 record.filename, record.funcName, 

479 record.lineno, message)