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

204 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-11 10:25 +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", "logf", "tracef", "debugf", "infof", "warnf", "errorf", "fatalf", 

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 logf(loggername, level, fmt, *args, **kwargs): 

328 Log.getLogger(loggername)._log(level, True, fmt, *args, **kwargs) 

329 

330 

331def tracef(fmt, *args, **kwargs): 

332 Log.getDefaultLogger()._log(TRACE, True, fmt, *args, **kwargs) 

333 

334 

335def debugf(fmt, *args, **kwargs): 

336 Log.getDefaultLogger()._log(DEBUG, True, fmt, *args, **kwargs) 

337 

338 

339def infof(fmt, *args, **kwargs): 

340 Log.getDefaultLogger()._log(INFO, True, fmt, *args, **kwargs) 

341 

342 

343def warnf(fmt, *args, **kwargs): 

344 Log.getDefaultLogger()._log(WARN, True, fmt, *args, **kwargs) 

345 

346 

347def errorf(fmt, *args, **kwargs): 

348 Log.getDefaultLogger()._log(ERROR, True, fmt, *args, **kwargs) 

349 

350 

351def fatalf(fmt, *args, **kwargs): 

352 Log.getDefaultLogger()._log(FATAL, True, fmt, *args, **kwargs) 

353 

354 

355def lwpID(): 

356 return Log.lwpID 

357 

358 

359def getLevelName(level): 

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

361 

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

363 """ 

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

365 for name in names: 

366 test_level = getattr(Log, name) 

367 if test_level == level: 

368 return name 

369 return f"Level {level}" 

370 

371 

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

373# UsePythonLogging and usePythonLogging. 

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

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

376def usePythonLogging(): 

377 Log.usePythonLogging() 

378 

379 

380def doNotUsePythonLogging(): 

381 Log.doNotUsePythonLogging() 

382 

383 

384class UsePythonLogging: 

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

386 """ 

387 

388 def __init__(self): 

389 self.current = Log.UsePythonLogging 

390 

391 def __enter__(self): 

392 Log.usePythonLogging() 

393 

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

395 Log.UsePythonLogging = self.current 

396 

397 

398class LevelTranslator: 

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

400 `logging`. 

401 """ 

402 @staticmethod 

403 def lsstLog2logging(level): 

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

405 

406 Parameters 

407 ---------- 

408 level : `int` 

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

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

411 

412 Returns 

413 ------- 

414 level : `int` 

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

416 """ 

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

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

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

420 return level//1000 

421 

422 @staticmethod 

423 def logging2lsstLog(level): 

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

425 lsst.log/log4cxx levels. 

426 

427 Parameters 

428 ---------- 

429 level : `int` 

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

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

432 `logging.INFO`, etc.) 

433 

434 Returns 

435 ------- 

436 level : `int` 

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

438 """ 

439 return level*1000 

440 

441 

442class LogHandler(logging.Handler): 

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

444 

445 Parameters 

446 ---------- 

447 level : `int` 

448 Level at which to set the this handler. 

449 

450 Notes 

451 ----- 

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

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

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

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

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

457 approach is to configure the logger with an additional handler 

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

459 Python logging. 

460 """ 

461 

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

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

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

465 # message a second time. 

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

467 

468 def handle(self, record): 

469 logger = Log.getLogger(record.name) 

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

471 logging.Handler.handle(self, record) 

472 

473 def emit(self, record): 

474 if Log.UsePythonLogging: 

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

476 # a logging loop. 

477 

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

479 # for this logger. 

480 pylgr = logging.getLogger(record.name) 

481 

482 # If another handler is registered that is not LogHandler 

483 # we ignore this request 

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

485 return 

486 

487 # If the parent has handlers and propagation is enabled 

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

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

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

491 return 

492 

493 # Force this message to appear somewhere. 

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

495 # second Handler. 

496 stream = logging.StreamHandler() 

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

498 stream.handle(record) 

499 return 

500 

501 logger = Log.getLogger(record.name) 

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

503 message = self.formatter.format(record) 

504 

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

506 record.filename, record.funcName, 

507 record.lineno, message)