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

223 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-10 10:32 +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 

38from deprecated.sphinx import deprecated 

39 

40from lsst.utils import continueClass 

41 

42from .log import Log 

43 

44TRACE = 5000 

45DEBUG = 10000 

46INFO = 20000 

47WARN = 30000 

48ERROR = 40000 

49FATAL = 50000 

50 

51# For compatibility with python logging 

52CRITICAL = FATAL 

53WARNING = WARN 

54 

55 

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

57class Log: # noqa: F811 

58 UsePythonLogging = False 

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

60 

61 CRITICAL = CRITICAL 

62 WARNING = WARNING 

63 

64 @classmethod 

65 def usePythonLogging(cls): 

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

67 

68 Notes 

69 ----- 

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

71 that log messages are captured by the testing environment 

72 as distinct from standard output. 

73 

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

75 package from Python. 

76 """ 

77 cls.UsePythonLogging = True 

78 

79 @classmethod 

80 def doNotUsePythonLogging(cls): 

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

82 

83 Notes 

84 ----- 

85 This is the default state. 

86 """ 

87 cls.UsePythonLogging = False 

88 

89 @property 

90 def name(self): 

91 return self.getName() 

92 

93 @property 

94 def level(self): 

95 return self.getLevel() 

96 

97 @property 

98 def parent(self): 

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

100 if not self.name: 

101 return None 

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

103 if not parent_name: 

104 return self.getDefaultLogger() 

105 return self.getLogger(parent_name) 

106 

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

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

109 

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

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

112 

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

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

115 

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

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

118 

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

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

121 # line number in the log. 

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

123 

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

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

126 

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

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

129 

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

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

132 # line number in the log. 

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

134 

135 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

136 " Will be removed after v25", 

137 version="v23.0", category=FutureWarning) 

138 def tracef(self, fmt, *args, **kwargs): 

139 self._log(Log.TRACE, True, fmt, *args, **kwargs) 

140 

141 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

142 " Will be removed after v25", 

143 version="v23.0", category=FutureWarning) 

144 def debugf(self, fmt, *args, **kwargs): 

145 self._log(Log.DEBUG, True, fmt, *args, **kwargs) 

146 

147 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

148 " Will be removed after v25", 

149 version="v23.0", category=FutureWarning) 

150 def infof(self, fmt, *args, **kwargs): 

151 self._log(Log.INFO, True, fmt, *args, **kwargs) 

152 

153 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

154 " Will be removed after v25", 

155 version="v23.0", category=FutureWarning) 

156 def warnf(self, fmt, *args, **kwargs): 

157 self._log(Log.WARN, True, fmt, *args, **kwargs) 

158 

159 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

160 " Will be removed after v25", 

161 version="v23.0", category=FutureWarning) 

162 def errorf(self, fmt, *args, **kwargs): 

163 self._log(Log.ERROR, True, fmt, *args, **kwargs) 

164 

165 @deprecated(reason="f-string log messages are now deprecated to match python logging convention." 

166 " Will be removed after v25", 

167 version="v23.0", category=FutureWarning) 

168 def fatalf(self, fmt, *args, **kwargs): 

169 self._log(Log.FATAL, True, fmt, *args, **kwargs) 

170 

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

172 if self.isEnabledFor(level): 

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

174 frame = frame.f_back # original log location 

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

176 funcname = frame.f_code.co_name 

177 if use_format: 

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

179 else: 

180 msg = fmt % args if args else fmt 

181 if self.UsePythonLogging: 

182 levelno = LevelTranslator.lsstLog2logging(level) 

183 levelName = logging.getLevelName(levelno) 

184 

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

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

187 levelno=levelno, 

188 levelname=levelName, 

189 msg=msg, 

190 funcName=funcname, 

191 filename=filename, 

192 pathname=frame.f_code.co_filename, 

193 lineno=frame.f_lineno)) 

194 pylog.handle(record) 

195 else: 

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

197 

198 def __reduce__(self): 

199 """Implement pickle support. 

200 """ 

201 args = (self.getName(), ) 

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

203 return (getLogger, args) 

204 

205 def __repr__(self): 

206 # Match python logging style. 

207 cls = type(self) 

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

209 prefix = "lsst.log.log.log" 

210 if class_name.startswith(prefix): 

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

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

213 

214 

215class MDCDict(dict): 

216 """Dictionary for MDC data. 

217 

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

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

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

221 """ 

222 def __getitem__(self, name: str): 

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

224 """ 

225 return self.get(name, "") 

226 

227 def __str__(self): 

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

229 quotes. 

230 """ 

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

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

233 

234 def __repr__(self): 

235 return str(self) 

236 

237 

238# Export static functions from Log class to module namespace 

239 

240 

241def configure(*args): 

242 Log.configure(*args) 

243 

244 

245def configure_prop(properties): 

246 Log.configure_prop(properties) 

247 

248 

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

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

251 

252 Parameters 

253 ---------- 

254 level : `str` 

255 Name of the logging level for root log4cxx logger. 

256 MDC_class : `type`, optional 

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

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

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

260 

261 Notes 

262 ----- 

263 This method does two things: 

264 

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

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

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

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

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

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

271 """ 

272 if MDC_class is not None: 

273 old_factory = logging.getLogRecordFactory() 

274 

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

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

277 record.MDC = MDC_class() 

278 return record 

279 

280 logging.setLogRecordFactory(record_factory) 

281 

282 properties = """\ 

283log4j.rootLogger = {}, PyLog 

284log4j.appender.PyLog = PyLogAppender 

285""".format(level) 

286 configure_prop(properties) 

287 

288 

289def getDefaultLogger(): 

290 return Log.getDefaultLogger() 

291 

292 

293def getLogger(loggername): 

294 return Log.getLogger(loggername) 

295 

296 

297def MDC(key, value): 

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

299 

300 

301def MDCRemove(key): 

302 Log.MDCRemove(key) 

303 

304 

305def MDCRegisterInit(func): 

306 Log.MDCRegisterInit(func) 

307 

308 

309def setLevel(loggername, level): 

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

311 

312 

313def getLevel(loggername): 

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

315 

316 

317def getEffectiveLevel(loggername): 

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

319 

320 

321def isEnabledFor(loggername, level): 

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

323 

324 

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

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

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

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

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

330 

331 

332def trace(fmt, *args): 

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

334 

335 

336def debug(fmt, *args): 

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

338 

339 

340def info(fmt, *args): 

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

342 

343 

344def warn(fmt, *args): 

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

346 

347 

348def warning(fmt, *args): 

349 warn(fmt, *args) 

350 

351 

352def error(fmt, *args): 

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

354 

355 

356def fatal(fmt, *args): 

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

358 

359 

360def critical(fmt, *args): 

361 fatal(fmt, *args) 

362 

363 

364def logf(loggername, level, fmt, *args, **kwargs): 

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

366 

367 

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

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

370 

371 

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

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

374 

375 

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

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

378 

379 

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

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

382 

383 

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

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

386 

387 

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

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

390 

391 

392def lwpID(): 

393 return Log.lwpID 

394 

395 

396def getLevelName(level): 

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

398 

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

400 """ 

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

402 for name in names: 

403 test_level = getattr(Log, name) 

404 if test_level == level: 

405 return name 

406 return f"Level {level}" 

407 

408 

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

410# UsePythonLogging and usePythonLogging. 

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

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

413def usePythonLogging(): 

414 Log.usePythonLogging() 

415 

416 

417def doNotUsePythonLogging(): 

418 Log.doNotUsePythonLogging() 

419 

420 

421class UsePythonLogging: 

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

423 """ 

424 

425 def __init__(self): 

426 self.current = Log.UsePythonLogging 

427 

428 def __enter__(self): 

429 Log.usePythonLogging() 

430 

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

432 Log.UsePythonLogging = self.current 

433 

434 

435class LevelTranslator: 

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

437 `logging`. 

438 """ 

439 @staticmethod 

440 def lsstLog2logging(level): 

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

442 

443 Parameters 

444 ---------- 

445 level : `int` 

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

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

448 

449 Returns 

450 ------- 

451 level : `int` 

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

453 """ 

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

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

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

457 return level//1000 

458 

459 @staticmethod 

460 def logging2lsstLog(level): 

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

462 lsst.log/log4cxx levels. 

463 

464 Parameters 

465 ---------- 

466 level : `int` 

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

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

469 `logging.INFO`, etc.) 

470 

471 Returns 

472 ------- 

473 level : `int` 

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

475 """ 

476 return level*1000 

477 

478 

479class LogHandler(logging.Handler): 

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

481 

482 Parameters 

483 ---------- 

484 level : `int` 

485 Level at which to set the this handler. 

486 

487 Notes 

488 ----- 

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

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

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

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

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

494 approach is to configure the logger with an additional handler 

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

496 Python logging. 

497 """ 

498 

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

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

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

502 # message a second time. 

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

504 

505 def handle(self, record): 

506 logger = Log.getLogger(record.name) 

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

508 logging.Handler.handle(self, record) 

509 

510 def emit(self, record): 

511 if Log.UsePythonLogging: 

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

513 # a logging loop. 

514 

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

516 # for this logger. 

517 pylgr = logging.getLogger(record.name) 

518 

519 # If another handler is registered that is not LogHandler 

520 # we ignore this request 

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

522 return 

523 

524 # If the parent has handlers and propagation is enabled 

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

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

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

528 return 

529 

530 # Force this message to appear somewhere. 

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

532 # second Handler. 

533 stream = logging.StreamHandler() 

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

535 stream.handle(record) 

536 return 

537 

538 logger = Log.getLogger(record.name) 

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

540 message = self.formatter.format(record) 

541 

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

543 record.filename, record.funcName, 

544 record.lineno, message)