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#!/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 deprecated.sphinx import deprecated 

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 self.warn(fmt, *args) 

120 

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

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

123 

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

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

126 

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

128 self.fatal(fmt, *args) 

129 

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

131 " Will be removed after v25", 

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

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

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

135 

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

137 " Will be removed after v25", 

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

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

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

141 

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

143 " Will be removed after v25", 

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

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

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

147 

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

149 " Will be removed after v25", 

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

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

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

153 

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

155 " Will be removed after v25", 

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

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

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

159 

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

161 " Will be removed after v25", 

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

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

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

165 

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

167 if self.isEnabledFor(level): 

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

169 frame = frame.f_back # original log location 

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

171 funcname = frame.f_code.co_name 

172 if use_format: 

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

174 else: 

175 msg = fmt % args if args else fmt 

176 if self.UsePythonLogging: 

177 levelno = LevelTranslator.lsstLog2logging(level) 

178 levelName = logging.getLevelName(levelno) 

179 

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

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

182 levelno=levelno, 

183 levelname=levelName, 

184 msg=msg, 

185 funcName=funcname, 

186 filename=filename, 

187 pathname=frame.f_code.co_filename, 

188 lineno=frame.f_lineno)) 

189 pylog.handle(record) 

190 else: 

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

192 

193 def __reduce__(self): 

194 """Implement pickle support. 

195 """ 

196 args = (self.getName(), ) 

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

198 return (getLogger, args) 

199 

200 def __repr__(self): 

201 # Match python logging style. 

202 cls = type(self) 

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

204 prefix = "lsst.log.log.log" 

205 if class_name.startswith(prefix): 

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

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

208 

209 

210class MDCDict(dict): 

211 """Dictionary for MDC data. 

212 

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

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

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

216 """ 

217 def __getitem__(self, name: str): 

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

219 """ 

220 return self.get(name, "") 

221 

222 def __str__(self): 

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

224 quotes. 

225 """ 

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

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

228 

229 def __repr__(self): 

230 return str(self) 

231 

232 

233# Export static functions from Log class to module namespace 

234 

235 

236def configure(*args): 

237 Log.configure(*args) 

238 

239 

240def configure_prop(properties): 

241 Log.configure_prop(properties) 

242 

243 

244def configure_pylog_MDC(level: str, MDC_class: type = MDCDict): 

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

246 

247 Parameters 

248 ---------- 

249 level : `str` 

250 Name of the logging level for root log4cxx logger. 

251 MDC_class : `type`, optional 

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

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

254 a type. 

255 

256 Notes 

257 ----- 

258 This method does two things: 

259 

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

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

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

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

264 ``MDC_class``). 

265 """ 

266 old_factory = logging.getLogRecordFactory() 

267 

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

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

270 record.MDC = MDC_class() 

271 return record 

272 

273 logging.setLogRecordFactory(record_factory) 

274 

275 properties = """\ 

276log4j.rootLogger = {}, PyLog 

277log4j.appender.PyLog = PyLogAppender 

278""".format(level) 

279 configure_prop(properties) 

280 

281 

282def getDefaultLogger(): 

283 return Log.getDefaultLogger() 

284 

285 

286def getLogger(loggername): 

287 return Log.getLogger(loggername) 

288 

289 

290def MDC(key, value): 

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

292 

293 

294def MDCRemove(key): 

295 Log.MDCRemove(key) 

296 

297 

298def MDCRegisterInit(func): 

299 Log.MDCRegisterInit(func) 

300 

301 

302def setLevel(loggername, level): 

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

304 

305 

306def getLevel(loggername): 

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

308 

309 

310def getEffectiveLevel(loggername): 

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

312 

313 

314def isEnabledFor(loggername, level): 

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

316 

317 

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

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

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

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

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

323 

324 

325def trace(fmt, *args): 

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

327 

328 

329def debug(fmt, *args): 

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

331 

332 

333def info(fmt, *args): 

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

335 

336 

337def warn(fmt, *args): 

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

339 

340 

341def warning(fmt, *args): 

342 warn(fmt, *args) 

343 

344 

345def error(fmt, *args): 

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

347 

348 

349def fatal(fmt, *args): 

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

351 

352 

353def critical(fmt, *args): 

354 fatal(fmt, *args) 

355 

356 

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

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

359 

360 

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

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

363 

364 

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

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

367 

368 

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

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

371 

372 

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

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

375 

376 

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

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

379 

380 

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

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

383 

384 

385def lwpID(): 

386 return Log.lwpID 

387 

388 

389def getLevelName(level): 

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

391 

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

393 """ 

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

395 for name in names: 

396 test_level = getattr(Log, name) 

397 if test_level == level: 

398 return name 

399 return f"Level {level}" 

400 

401 

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

403# UsePythonLogging and usePythonLogging. 

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

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

406def usePythonLogging(): 

407 Log.usePythonLogging() 

408 

409 

410def doNotUsePythonLogging(): 

411 Log.doNotUsePythonLogging() 

412 

413 

414class UsePythonLogging: 

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

416 """ 

417 

418 def __init__(self): 

419 self.current = Log.UsePythonLogging 

420 

421 def __enter__(self): 

422 Log.usePythonLogging() 

423 

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

425 Log.UsePythonLogging = self.current 

426 

427 

428class LevelTranslator: 

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

430 `logging`. 

431 """ 

432 @staticmethod 

433 def lsstLog2logging(level): 

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

435 

436 Parameters 

437 ---------- 

438 level : `int` 

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

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

441 

442 Returns 

443 ------- 

444 level : `int` 

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

446 """ 

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

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

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

450 return level//1000 

451 

452 @staticmethod 

453 def logging2lsstLog(level): 

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

455 lsst.log/log4cxx levels. 

456 

457 Parameters 

458 ---------- 

459 level : `int` 

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

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

462 `logging.INFO`, etc.) 

463 

464 Returns 

465 ------- 

466 level : `int` 

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

468 """ 

469 return level*1000 

470 

471 

472class LogHandler(logging.Handler): 

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

474 

475 Parameters 

476 ---------- 

477 level : `int` 

478 Level at which to set the this handler. 

479 

480 Notes 

481 ----- 

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

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

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

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

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

487 approach is to configure the logger with an additional handler 

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

489 Python logging. 

490 """ 

491 

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

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

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

495 # message a second time. 

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

497 

498 def handle(self, record): 

499 logger = Log.getLogger(record.name) 

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

501 logging.Handler.handle(self, record) 

502 

503 def emit(self, record): 

504 if Log.UsePythonLogging: 

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

506 # a logging loop. 

507 

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

509 # for this logger. 

510 pylgr = logging.getLogger(record.name) 

511 

512 # If another handler is registered that is not LogHandler 

513 # we ignore this request 

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

515 return 

516 

517 # If the parent has handlers and propagation is enabled 

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

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

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

521 return 

522 

523 # Force this message to appear somewhere. 

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

525 # second Handler. 

526 stream = logging.StreamHandler() 

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

528 stream.handle(record) 

529 return 

530 

531 logger = Log.getLogger(record.name) 

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

533 message = self.formatter.format(record) 

534 

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

536 record.filename, record.funcName, 

537 record.lineno, message)