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

121 

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

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

124 

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

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

127 

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

129 self.fatal(fmt, *args) 

130 

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

132 " Will be removed after v25", 

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

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

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

136 

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

138 " Will be removed after v25", 

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

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

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

142 

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

144 " Will be removed after v25", 

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

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

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

148 

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

150 " Will be removed after v25", 

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

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

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

154 

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

156 " Will be removed after v25", 

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

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

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

160 

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

162 " Will be removed after v25", 

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

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

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

166 

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

168 if self.isEnabledFor(level): 

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

170 frame = frame.f_back # original log location 

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

172 funcname = frame.f_code.co_name 

173 if use_format: 

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

175 else: 

176 msg = fmt % args if args else fmt 

177 if self.UsePythonLogging: 

178 levelno = LevelTranslator.lsstLog2logging(level) 

179 levelName = logging.getLevelName(levelno) 

180 

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

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

183 levelno=levelno, 

184 levelname=levelName, 

185 msg=msg, 

186 funcName=funcname, 

187 filename=filename, 

188 pathname=frame.f_code.co_filename, 

189 lineno=frame.f_lineno)) 

190 pylog.handle(record) 

191 else: 

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

193 

194 def __reduce__(self): 

195 """Implement pickle support. 

196 """ 

197 args = (self.getName(), ) 

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

199 return (getLogger, args) 

200 

201 def __repr__(self): 

202 # Match python logging style. 

203 cls = type(self) 

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

205 prefix = "lsst.log.log.log" 

206 if class_name.startswith(prefix): 

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

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

209 

210 

211class MDCDict(dict): 

212 """Dictionary for MDC data. 

213 

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

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

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

217 """ 

218 def __getitem__(self, name: str): 

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

220 """ 

221 return self.get(name, "") 

222 

223 def __str__(self): 

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

225 quotes. 

226 """ 

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

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

229 

230 def __repr__(self): 

231 return str(self) 

232 

233 

234# Export static functions from Log class to module namespace 

235 

236 

237def configure(*args): 

238 Log.configure(*args) 

239 

240 

241def configure_prop(properties): 

242 Log.configure_prop(properties) 

243 

244 

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

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

247 

248 Parameters 

249 ---------- 

250 level : `str` 

251 Name of the logging level for root log4cxx logger. 

252 MDC_class : `type`, optional 

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

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

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

256 

257 Notes 

258 ----- 

259 This method does two things: 

260 

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

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

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

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

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

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

267 """ 

268 if MDC_class is not None: 

269 old_factory = logging.getLogRecordFactory() 

270 

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

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

273 record.MDC = MDC_class() 

274 return record 

275 

276 logging.setLogRecordFactory(record_factory) 

277 

278 properties = """\ 

279log4j.rootLogger = {}, PyLog 

280log4j.appender.PyLog = PyLogAppender 

281""".format(level) 

282 configure_prop(properties) 

283 

284 

285def getDefaultLogger(): 

286 return Log.getDefaultLogger() 

287 

288 

289def getLogger(loggername): 

290 return Log.getLogger(loggername) 

291 

292 

293def MDC(key, value): 

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

295 

296 

297def MDCRemove(key): 

298 Log.MDCRemove(key) 

299 

300 

301def MDCRegisterInit(func): 

302 Log.MDCRegisterInit(func) 

303 

304 

305def setLevel(loggername, level): 

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

307 

308 

309def getLevel(loggername): 

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

311 

312 

313def getEffectiveLevel(loggername): 

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

315 

316 

317def isEnabledFor(loggername, level): 

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

319 

320 

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

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

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

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

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

326 

327 

328def trace(fmt, *args): 

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

330 

331 

332def debug(fmt, *args): 

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

334 

335 

336def info(fmt, *args): 

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

338 

339 

340def warn(fmt, *args): 

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

342 

343 

344def warning(fmt, *args): 

345 warn(fmt, *args) 

346 

347 

348def error(fmt, *args): 

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

350 

351 

352def fatal(fmt, *args): 

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

354 

355 

356def critical(fmt, *args): 

357 fatal(fmt, *args) 

358 

359 

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

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

362 

363 

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

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

366 

367 

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

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

370 

371 

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

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

374 

375 

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

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

378 

379 

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

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

382 

383 

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

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

386 

387 

388def lwpID(): 

389 return Log.lwpID 

390 

391 

392def getLevelName(level): 

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

394 

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

396 """ 

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

398 for name in names: 

399 test_level = getattr(Log, name) 

400 if test_level == level: 

401 return name 

402 return f"Level {level}" 

403 

404 

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

406# UsePythonLogging and usePythonLogging. 

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

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

409def usePythonLogging(): 

410 Log.usePythonLogging() 

411 

412 

413def doNotUsePythonLogging(): 

414 Log.doNotUsePythonLogging() 

415 

416 

417class UsePythonLogging: 

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

419 """ 

420 

421 def __init__(self): 

422 self.current = Log.UsePythonLogging 

423 

424 def __enter__(self): 

425 Log.usePythonLogging() 

426 

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

428 Log.UsePythonLogging = self.current 

429 

430 

431class LevelTranslator: 

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

433 `logging`. 

434 """ 

435 @staticmethod 

436 def lsstLog2logging(level): 

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

438 

439 Parameters 

440 ---------- 

441 level : `int` 

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

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

444 

445 Returns 

446 ------- 

447 level : `int` 

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

449 """ 

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

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

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

453 return level//1000 

454 

455 @staticmethod 

456 def logging2lsstLog(level): 

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

458 lsst.log/log4cxx levels. 

459 

460 Parameters 

461 ---------- 

462 level : `int` 

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

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

465 `logging.INFO`, etc.) 

466 

467 Returns 

468 ------- 

469 level : `int` 

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

471 """ 

472 return level*1000 

473 

474 

475class LogHandler(logging.Handler): 

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

477 

478 Parameters 

479 ---------- 

480 level : `int` 

481 Level at which to set the this handler. 

482 

483 Notes 

484 ----- 

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

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

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

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

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

490 approach is to configure the logger with an additional handler 

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

492 Python logging. 

493 """ 

494 

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

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

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

498 # message a second time. 

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

500 

501 def handle(self, record): 

502 logger = Log.getLogger(record.name) 

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

504 logging.Handler.handle(self, record) 

505 

506 def emit(self, record): 

507 if Log.UsePythonLogging: 

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

509 # a logging loop. 

510 

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

512 # for this logger. 

513 pylgr = logging.getLogger(record.name) 

514 

515 # If another handler is registered that is not LogHandler 

516 # we ignore this request 

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

518 return 

519 

520 # If the parent has handlers and propagation is enabled 

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

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

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

524 return 

525 

526 # Force this message to appear somewhere. 

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

528 # second Handler. 

529 stream = logging.StreamHandler() 

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

531 stream.handle(record) 

532 return 

533 

534 logger = Log.getLogger(record.name) 

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

536 message = self.formatter.format(record) 

537 

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

539 record.filename, record.funcName, 

540 record.lineno, message)