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", 

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", "logf", "tracef", "debugf", "infof", "warnf", "errorf", "fatalf", 

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

31 "LevelTranslator", "LogHandler"] 

32 

33import logging 

34import inspect 

35import os 

36 

37from lsst.utils import continueClass 

38 

39from .log import Log 

40 

41TRACE = 5000 

42DEBUG = 10000 

43INFO = 20000 

44WARN = 30000 

45ERROR = 40000 

46FATAL = 50000 

47 

48 

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

50class Log: # noqa: F811 

51 UsePythonLogging = False 

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

53 

54 @classmethod 

55 def usePythonLogging(cls): 

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

57 

58 Notes 

59 ----- 

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

61 that log messages are captured by the testing environment 

62 as distinct from standard output. 

63 

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

65 package from Python. 

66 """ 

67 cls.UsePythonLogging = True 

68 

69 @classmethod 

70 def doNotUsePythonLogging(cls): 

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

72 

73 Notes 

74 ----- 

75 This is the default state. 

76 """ 

77 cls.UsePythonLogging = False 

78 

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

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

81 

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

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

84 

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

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

87 

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

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

90 

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

92 self.warn(fmt, *args) 

93 

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

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

96 

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

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

99 

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

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

102 

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

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

105 

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

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

108 

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

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

111 

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

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

114 

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

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

117 

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

119 if self.isEnabledFor(level): 

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

121 frame = frame.f_back # original log location 

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

123 funcname = frame.f_code.co_name 

124 if use_format: 

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

126 else: 

127 msg = fmt % args if args else fmt 

128 if self.UsePythonLogging: 

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

130 record = logging.LogRecord(self.getName(), LevelTranslator.lsstLog2logging(level), 

131 filename, frame.f_lineno, msg, None, False, func=funcname) 

132 pylog.handle(record) 

133 else: 

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

135 

136 def __reduce__(self): 

137 """Implement pickle support. 

138 """ 

139 args = (self.getName(), ) 

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

141 return (getLogger, args) 

142 

143 

144class MDCDict(dict): 

145 """Dictionary for MDC data. 

146 

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

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

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

150 """ 

151 def __getitem__(self, name: str): 

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

153 """ 

154 return self.get(name, "") 

155 

156 def __str__(self): 

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

158 quotes. 

159 """ 

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

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

162 

163 def __repr__(self): 

164 return str(self) 

165 

166 

167# Export static functions from Log class to module namespace 

168 

169 

170def configure(*args): 

171 Log.configure(*args) 

172 

173 

174def configure_prop(properties): 

175 Log.configure_prop(properties) 

176 

177 

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

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

180 

181 Parameters 

182 ---------- 

183 level : `str` 

184 Name of the logging level for root log4cxx logger. 

185 MDC_class : `type`, optional 

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

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

188 a type. 

189 

190 Notes 

191 ----- 

192 This method does two things: 

193 

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

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

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

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

198 ``MDC_class``). 

199 """ 

200 old_factory = logging.getLogRecordFactory() 

201 

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

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

204 record.MDC = MDC_class() 

205 return record 

206 

207 logging.setLogRecordFactory(record_factory) 

208 

209 properties = """\ 

210log4j.rootLogger = {}, PyLog 

211log4j.appender.PyLog = PyLogAppender 

212""".format(level) 

213 configure_prop(properties) 

214 

215 

216def getDefaultLogger(): 

217 return Log.getDefaultLogger() 

218 

219 

220def getLogger(loggername): 

221 return Log.getLogger(loggername) 

222 

223 

224def MDC(key, value): 

225 Log.MDC(key, str(value)) 

226 

227 

228def MDCRemove(key): 

229 Log.MDCRemove(key) 

230 

231 

232def MDCRegisterInit(func): 

233 Log.MDCRegisterInit(func) 

234 

235 

236def setLevel(loggername, level): 

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

238 

239 

240def getLevel(loggername): 

241 Log.getLogger(loggername).getLevel() 

242 

243 

244def isEnabledFor(logger, level): 

245 Log.getLogger(logger).isEnabledFor(level) 

246 

247 

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

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

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

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

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

253 

254 

255def trace(fmt, *args): 

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

257 

258 

259def debug(fmt, *args): 

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

261 

262 

263def info(fmt, *args): 

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

265 

266 

267def warn(fmt, *args): 

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

269 

270 

271def warning(fmt, *args): 

272 warn(fmt, *args) 

273 

274 

275def error(fmt, *args): 

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

277 

278 

279def fatal(fmt, *args): 

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

281 

282 

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

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

285 

286 

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

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

289 

290 

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

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

293 

294 

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

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

297 

298 

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

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

301 

302 

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

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

305 

306 

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

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

309 

310 

311def lwpID(): 

312 return Log.lwpID 

313 

314 

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

316# UsePythonLogging and usePythonLogging. 

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

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

319def usePythonLogging(): 

320 Log.usePythonLogging() 

321 

322 

323def doNotUsePythonLogging(): 

324 Log.doNotUsePythonLogging() 

325 

326 

327class UsePythonLogging: 

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

329 """ 

330 

331 def __init__(self): 

332 self.current = Log.UsePythonLogging 

333 

334 def __enter__(self): 

335 Log.usePythonLogging() 

336 

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

338 Log.UsePythonLogging = self.current 

339 

340 

341class LevelTranslator: 

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

343 `logging`. 

344 """ 

345 @staticmethod 

346 def lsstLog2logging(level): 

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

348 

349 Parameters 

350 ---------- 

351 level : `int` 

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

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

354 

355 Returns 

356 ------- 

357 level : `int` 

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

359 """ 

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

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

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

363 return level//1000 

364 

365 @staticmethod 

366 def logging2lsstLog(level): 

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

368 lsst.log/log4cxx levels. 

369 

370 Parameters 

371 ---------- 

372 level : `int` 

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

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

375 `logging.INFO`, etc.) 

376 

377 Returns 

378 ------- 

379 level : `int` 

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

381 """ 

382 return level*1000 

383 

384 

385class LogHandler(logging.Handler): 

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

387 

388 Parameters 

389 ---------- 

390 level : `int` 

391 Level at which to set the this handler. 

392 

393 Notes 

394 ----- 

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

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

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

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

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

400 approach is to configure the logger with an additional handler 

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

402 Python logging. 

403 """ 

404 

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

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

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

408 # message a second time. 

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

410 

411 def handle(self, record): 

412 logger = Log.getLogger(record.name) 

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

414 logging.Handler.handle(self, record) 

415 

416 def emit(self, record): 

417 if Log.UsePythonLogging: 

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

419 # a logging loop. 

420 

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

422 # for this logger. 

423 pylgr = logging.getLogger(record.name) 

424 

425 # If another handler is registered that is not LogHandler 

426 # we ignore this request 

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

428 return 

429 

430 # If the parent has handlers and propagation is enabled 

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

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

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

434 return 

435 

436 # Force this message to appear somewhere. 

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

438 # second Handler. 

439 stream = logging.StreamHandler() 

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

441 stream.handle(record) 

442 return 

443 

444 logger = Log.getLogger(record.name) 

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

446 message = self.formatter.format(record) 

447 

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

449 record.filename, record.funcName, 

450 record.lineno, message)