Coverage for tests/test_log.py: 12%

Shortcuts 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

367 statements  

1 

2# LSST Data Management System 

3# Copyright 2014-2017 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21 

22""" 

23This tests the logging system in a variety of ways. 

24""" 

25 

26 

27import os 

28import shutil 

29import tempfile 

30import threading 

31import unittest 

32import logging 

33import lsst.log as log 

34 

35 

36class TestLog(unittest.TestCase): 

37 

38 class StdoutCapture(object): 

39 """ 

40 Context manager to redirect stdout to a file. 

41 """ 

42 

43 def __init__(self, filename): 

44 self.stdout = None 

45 self.outputFilename = filename 

46 

47 def __enter__(self): 

48 self.stdout = os.dup(1) 

49 os.close(1) 

50 os.open(self.outputFilename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) 

51 

52 def __exit__(self, type, value, traceback): 

53 if self.stdout is not None: 

54 os.close(1) 

55 os.dup(self.stdout) 

56 os.close(self.stdout) 

57 self.stdout = None 

58 

59 def setUp(self): 

60 """Make a temporary directory and a log file in it.""" 

61 self.tempDir = tempfile.mkdtemp() 

62 self.outputFilename = os.path.join(self.tempDir, "log.out") 

63 self.stdout = None 

64 

65 def tearDown(self): 

66 """Remove the temporary directory and clean up Python forwarding.""" 

67 log.doNotUsePythonLogging() 

68 shutil.rmtree(self.tempDir) 

69 

70 def configure(self, configuration): 

71 """ 

72 Create a configuration file in the temporary directory and populate 

73 it with the provided string. 

74 """ 

75 log.configure_prop(configuration.format(self.outputFilename)) 

76 

77 def check(self, reference): 

78 """Compare the log file with the provided reference text.""" 

79 with open(self.outputFilename, 'r') as f: 

80 # strip everything up to first ] to remove timestamp and thread ID 

81 lines = [line.split(']')[-1].rstrip("\n") for line in f.readlines()] 

82 reflines = [line for line in reference.split("\n") if line != ""] 

83 self.maxDiff = None 

84 self.assertListEqual(lines, reflines) 

85 

86 def testDefaultLogger(self): 

87 """Check the default root logger name.""" 

88 self.assertEqual(log.getDefaultLogger().getName(), "") 

89 

90 def testBasic(self): 

91 """ 

92 Test basic log output with default configuration. 

93 Since the default threshold is INFO, the DEBUG or TRACE 

94 message is not emitted. 

95 """ 

96 with TestLog.StdoutCapture(self.outputFilename): 

97 log.configure() 

98 log.log(log.getDefaultLogger(), log.INFO, "This is INFO") 

99 log.info(u"This is unicode INFO") 

100 log.trace("This is TRACE") 

101 log.debug("This is DEBUG") 

102 log.warn("This is WARN") 

103 log.error("This is ERROR") 

104 log.fatal("This is FATAL") 

105 log.critical("This is CRITICAL") 

106 log.warning("Format %d %g %s", 3, 2.71828, "foo") 

107 self.check(""" 

108root INFO: This is INFO 

109root INFO: This is unicode INFO 

110root WARN: This is WARN 

111root ERROR: This is ERROR 

112root FATAL: This is FATAL 

113root FATAL: This is CRITICAL 

114root WARN: Format 3 2.71828 foo 

115""") 

116 

117 def testBasicFormat(self): 

118 """ 

119 Test basic log output with default configuration but using 

120 the f variants. 

121 Since the default threshold is INFO, the DEBUG or TRACE 

122 message is not emitted. 

123 """ 

124 with TestLog.StdoutCapture(self.outputFilename): 

125 log.configure() 

126 log.logf(log.getDefaultLogger(), log.INFO, 

127 "This is {{INFO}} Item 1: {item[1]}", 

128 item=["a", "b", "c"]) 

129 log.infof(u"This is {unicode} INFO") 

130 log.tracef("This is TRACE") 

131 log.debugf("This is DEBUG") 

132 log.warnf("This is WARN {city}", city="Tucson") 

133 log.errorf("This is ERROR {1}->{0}", 2, 1) 

134 log.fatalf("This is FATAL {1} out of {0} times for {place}", 

135 4, 3, place="LSST") 

136 log.warnf("Format {} {} {}", 3, 2.71828, "foo") 

137 self.check(""" 

138root INFO: This is {INFO} Item 1: b 

139root INFO: This is {unicode} INFO 

140root WARN: This is WARN Tucson 

141root ERROR: This is ERROR 1->2 

142root FATAL: This is FATAL 3 out of 4 times for LSST 

143root WARN: Format 3 2.71828 foo 

144""") 

145 

146 def testPattern(self): 

147 """ 

148 Test a complex pattern for log messages, including Mapped 

149 Diagnostic Context (MDC). 

150 """ 

151 with TestLog.StdoutCapture(self.outputFilename): 

152 self.configure(""" 

153log4j.rootLogger=DEBUG, CA 

154log4j.appender.CA=ConsoleAppender 

155log4j.appender.CA.layout=PatternLayout 

156log4j.appender.CA.layout.ConversionPattern=%-5p %c %C %M (%F:%L) %l - %m - %X%n 

157""") 

158 log.trace("This is TRACE") 

159 log.info("This is INFO") 

160 log.debug("This is DEBUG") 

161 

162 log.MDC("x", 3) 

163 log.MDC("y", "foo") 

164 log.MDC("z", TestLog) 

165 

166 log.trace("This is TRACE 2") 

167 log.info("This is INFO 2") 

168 log.debug("This is DEBUG 2") 

169 log.MDCRemove("z") 

170 

171 log.trace("This is TRACE 3") 

172 log.info("This is INFO 3") 

173 log.debug("This is DEBUG 3") 

174 log.MDCRemove("x") 

175 log.trace("This is TRACE 4") 

176 log.info("This is INFO 4") 

177 log.debug("This is DEBUG 4") 

178 

179 log.trace("This is TRACE 5") 

180 log.info("This is INFO 5") 

181 log.debug("This is DEBUG 5") 

182 

183 log.MDCRemove("y") 

184 

185 # Use format to make line numbers easier to change. 

186 self.check(""" 

187INFO root testPattern (test_log.py:{0[0]}) test_log.py({0[0]}) - This is INFO - {{}} 

188DEBUG root testPattern (test_log.py:{0[1]}) test_log.py({0[1]}) - This is DEBUG - {{}} 

189INFO root testPattern (test_log.py:{0[2]}) test_log.py({0[2]}) - This is INFO 2 - {{{{x,3}}{{y,foo}}{{z,<class '{1}.TestLog'>}}}} 

190DEBUG root testPattern (test_log.py:{0[3]}) test_log.py({0[3]}) - This is DEBUG 2 - {{{{x,3}}{{y,foo}}{{z,<class '{1}.TestLog'>}}}} 

191INFO root testPattern (test_log.py:{0[4]}) test_log.py({0[4]}) - This is INFO 3 - {{{{x,3}}{{y,foo}}}} 

192DEBUG root testPattern (test_log.py:{0[5]}) test_log.py({0[5]}) - This is DEBUG 3 - {{{{x,3}}{{y,foo}}}} 

193INFO root testPattern (test_log.py:{0[6]}) test_log.py({0[6]}) - This is INFO 4 - {{{{y,foo}}}} 

194DEBUG root testPattern (test_log.py:{0[7]}) test_log.py({0[7]}) - This is DEBUG 4 - {{{{y,foo}}}} 

195INFO root testPattern (test_log.py:{0[8]}) test_log.py({0[8]}) - This is INFO 5 - {{{{y,foo}}}} 

196DEBUG root testPattern (test_log.py:{0[9]}) test_log.py({0[9]}) - This is DEBUG 5 - {{{{y,foo}}}} 

197""".format([x + 159 for x in (0, 1, 8, 9, 13, 14, 17, 18, 21, 22)], __name__)) # noqa E501 line too long 

198 

199 def testMDCPutPid(self): 

200 """ 

201 Test add of PID Mapped Diagnostic Context (MDC). 

202 """ 

203 pid = os.fork() 

204 try: 

205 

206 log.MDC("PID", os.getpid()) 

207 self.configure(""" 

208log4j.rootLogger=DEBUG, CA 

209log4j.appender.CA=ConsoleAppender 

210log4j.appender.CA.layout=PatternLayout 

211log4j.appender.CA.layout.ConversionPattern=%-5p PID:%X{{PID}} %c %C %M (%F:%L) %l - %m%n 

212""") # noqa E501 line too long 

213 self.assertGreaterEqual(pid, 0, "Failed to fork") 

214 

215 msg = "This is INFO" 

216 if pid == 0: 

217 self.tempDir = tempfile.mkdtemp() 

218 self.outputFilename = os.path.join(self.tempDir, 

219 "log-child.out") 

220 msg += " in child process" 

221 elif pid > 0: 

222 child_pid, child_status = os.wait() 

223 self.assertEqual(child_status, 0, 

224 "Child returns incorrect code") 

225 msg += " in parent process" 

226 

227 with TestLog.StdoutCapture(self.outputFilename): 

228 log.info(msg) 

229 line = 228 # line number for previous line 

230 finally: 

231 log.MDCRemove("PID") 

232 

233 # Use format to make line numbers easier to change. 

234 self.check(""" 

235INFO PID:{1} root testMDCPutPid (test_log.py:{0}) test_log.py({0}) - {2} 

236""".format(line, os.getpid(), msg)) 

237 

238 # don't pass other tests in child process 

239 if pid == 0: 

240 os._exit(0) 

241 

242 def testFileAppender(self): 

243 """Test configuring logging to go to a file.""" 

244 self.configure(""" 

245log4j.rootLogger=DEBUG, FA 

246log4j.appender.FA=FileAppender 

247log4j.appender.FA.file={0} 

248log4j.appender.FA.layout=SimpleLayout 

249""") 

250 log.MDC("x", 3) 

251 log.trace("This is TRACE") 

252 log.info("This is INFO") 

253 log.debug("This is DEBUG") 

254 log.MDCRemove("x") 

255 

256 self.check(""" 

257INFO - This is INFO 

258DEBUG - This is DEBUG 

259""") 

260 

261 def testPythonLogging(self): 

262 """Test logging through the Python logging interface.""" 

263 with TestLog.StdoutCapture(self.outputFilename): 

264 lgr = logging.getLogger() 

265 lgr.setLevel(logging.INFO) 

266 log.configure() 

267 with self.assertLogs(level="INFO") as cm: 

268 # Force the lsst.log handler to be applied as well as the 

269 # unittest log handler 

270 lgr.addHandler(log.LogHandler()) 

271 lgr.info("This is INFO") 

272 lgr.debug("This is DEBUG") 

273 lgr.warning("This is %s", "WARNING") 

274 # message can be arbitrary Python object 

275 lgr.info(((1, 2), (3, 4))) 

276 lgr.info({1: 2}) 

277 

278 # Confirm that Python logging also worked 

279 self.assertEqual(len(cm.output), 4, f"Got output: {cm.output}") 

280 logging.shutdown() 

281 

282 self.check(""" 

283root INFO: This is INFO 

284root WARN: This is WARNING 

285root INFO: ((1, 2), (3, 4)) 

286root INFO: {1: 2} 

287""") 

288 

289 def testMdcInit(self): 

290 

291 expected_msg = \ 

292 "INFO - main thread {{MDC_INIT,OK}}\n" + \ 

293 "INFO - thread 1 {{MDC_INIT,OK}}\n" + \ 

294 "INFO - thread 2 {{MDC_INIT,OK}}\n" 

295 

296 with TestLog.StdoutCapture(self.outputFilename): 

297 

298 self.configure(""" 

299log4j.rootLogger=DEBUG, CA 

300log4j.appender.CA=ConsoleAppender 

301log4j.appender.CA.layout=PatternLayout 

302log4j.appender.CA.layout.ConversionPattern=%-5p - %m %X%n 

303""") 

304 

305 def fun(): 

306 log.MDC("MDC_INIT", "OK") 

307 log.MDCRegisterInit(fun) 

308 

309 log.info("main thread") 

310 

311 thread = threading.Thread(target=lambda: log.info("thread 1")) 

312 thread.start() 

313 thread.join() 

314 

315 thread = threading.Thread(target=lambda: log.info("thread 2")) 

316 thread.start() 

317 thread.join() 

318 

319 self.check(expected_msg) 

320 

321 log.MDCRemove("MDC_INIT") 

322 

323 def testMdcUpdate(self): 

324 """Test for overwriting MDC. 

325 """ 

326 

327 expected_msg = \ 

328 "INFO - Message one {}\n" \ 

329 "INFO - Message two {{LABEL,123456}}\n" \ 

330 "INFO - Message three {{LABEL,654321}}\n" \ 

331 "INFO - Message four {}\n" 

332 

333 with TestLog.StdoutCapture(self.outputFilename): 

334 

335 self.configure(""" 

336log4j.rootLogger=DEBUG, CA 

337log4j.appender.CA=ConsoleAppender 

338log4j.appender.CA.layout=PatternLayout 

339log4j.appender.CA.layout.ConversionPattern=%-5p - %m %X%n 

340""") 

341 

342 log.info("Message one") 

343 

344 log.MDC("LABEL", "123456") 

345 log.info("Message two") 

346 

347 log.MDC("LABEL", "654321") 

348 log.info("Message three") 

349 

350 log.MDCRemove("LABEL") 

351 log.info("Message four") 

352 

353 self.check(expected_msg) 

354 

355 def testLwpID(self): 

356 """Test log.lwpID() method.""" 

357 lwp1 = log.lwpID() 

358 lwp2 = log.lwpID() 

359 

360 self.assertEqual(lwp1, lwp2) 

361 

362 def testLogger(self): 

363 """ 

364 Test log object. 

365 """ 

366 with TestLog.StdoutCapture(self.outputFilename): 

367 log.configure() 

368 logger = log.Log.getLogger("b") 

369 self.assertEqual(logger.getName(), "b") 

370 self.assertEqual(logger.name, logger.getName()) 

371 logger.trace("This is TRACE") 

372 logger.info("This is INFO") 

373 logger.debug("This is DEBUG") 

374 logger.warn("This is WARN") 

375 logger.error("This is ERROR") 

376 logger.fatal("This is FATAL") 

377 logger.warn("Format %d %g %s", 3, 2.71828, "foo") 

378 self.check(""" 

379b INFO: This is INFO 

380b WARN: This is WARN 

381b ERROR: This is ERROR 

382b FATAL: This is FATAL 

383b WARN: Format 3 2.71828 foo 

384""") 

385 

386 def testLoggerLevel(self): 

387 """ 

388 Test levels of Log objects 

389 """ 

390 with TestLog.StdoutCapture(self.outputFilename): 

391 self.configure(""" 

392log4j.rootLogger=TRACE, CA 

393log4j.appender.CA=ConsoleAppender 

394log4j.appender.CA.layout=PatternLayout 

395log4j.appender.CA.layout.ConversionPattern=%-5p %c (%F)- %m%n 

396""") 

397 self.assertEqual(log.Log.getLevel(log.Log.getDefaultLogger()), 

398 log.TRACE) 

399 self.assertEqual(log.Log.getDefaultLogger().getEffectiveLevel(), log.TRACE) 

400 logger = log.Log.getLogger("a.b") 

401 self.assertEqual(logger.getName(), "a.b") 

402 self.assertEqual(logger.getEffectiveLevel(), log.TRACE) 

403 self.assertEqual(log.getEffectiveLevel(logger.name), log.TRACE) 

404 logger.trace("This is TRACE") 

405 logger.setLevel(log.INFO) 

406 self.assertEqual(logger.getLevel(), log.INFO) 

407 self.assertEqual(logger.getEffectiveLevel(), log.INFO) 

408 self.assertEqual(logger.level, logger.getLevel()) 

409 self.assertEqual(log.Log.getLevel(logger), log.INFO) 

410 logger.debug("This is DEBUG") 

411 logger.info("This is INFO") 

412 logger.fatal("Format %d %g %s", 3, 2.71828, "foo") 

413 

414 logger = log.Log.getLogger("a.b.c") 

415 self.assertEqual(logger.getName(), "a.b.c") 

416 logger.trace("This is TRACE") 

417 logger.debug("This is DEBUG") 

418 logger.warn("This is WARN") 

419 logger.error("This is ERROR") 

420 logger.fatal("This is FATAL") 

421 logger.info("Format %d %g %s", 3, 2.71828, "foo") 

422 self.check(""" 

423TRACE a.b (test_log.py)- This is TRACE 

424INFO a.b (test_log.py)- This is INFO 

425FATAL a.b (test_log.py)- Format 3 2.71828 foo 

426WARN a.b.c (test_log.py)- This is WARN 

427ERROR a.b.c (test_log.py)- This is ERROR 

428FATAL a.b.c (test_log.py)- This is FATAL 

429INFO a.b.c (test_log.py)- Format 3 2.71828 foo 

430""") 

431 

432 def testMsgWithPercentS(self): 

433 """Test logging messages containing %s (DM-7509) 

434 """ 

435 with TestLog.StdoutCapture(self.outputFilename): 

436 log.configure() 

437 logger = log.Log() 

438 logger.info("INFO with %s") 

439 logger.trace("TRACE with %s") 

440 logger.debug("DEBUG with %s") 

441 logger.warn("WARN with %s") 

442 logger.error("ERROR with %s") 

443 logger.fatal("FATAL with %s") 

444 logger.logMsg(log.DEBUG, "foo", "bar", 5, "DEBUG with %s") 

445 self.check(""" 

446root INFO: INFO with %s 

447root WARN: WARN with %s 

448root ERROR: ERROR with %s 

449root FATAL: FATAL with %s 

450root DEBUG: DEBUG with %s 

451""") 

452 

453 def testForwardToPython(self): 

454 """Test that `lsst.log` log messages can be forwarded to `logging`.""" 

455 log.configure() 

456 

457 # Without forwarding we only get python logger messages captured 

458 with self.assertLogs(level="WARNING") as cm: 

459 log.warn("lsst.log warning message that will not be forwarded to Python") 

460 logging.warning("Python logging message that will be captured") 

461 self.assertEqual(len(cm.output), 1) 

462 

463 log.usePythonLogging() 

464 

465 # With forwarding we get 2 logging messages captured 

466 with self.assertLogs(level="WARNING") as cm: 

467 log.warn("This is a warning from lsst log meant for python logging") 

468 logging.warning("Python warning log message to be captured") 

469 self.assertEqual(len(cm.output), 2) 

470 

471 loggername = "newlogger" 

472 log2 = log.Log.getLogger(loggername) 

473 with self.assertLogs(level="INFO", logger=loggername): 

474 log2.info("Info message to non-root lsst logger") 

475 

476 # Check that debug and info are working properly 

477 # This test should return a single log message 

478 with self.assertLogs(level="INFO", logger=loggername) as cm: 

479 log2.info("Second INFO message to non-root lsst logger") 

480 log.debug("Debug message to root lsst logger") 

481 

482 self.assertEqual(len(cm.output), 1, f"Got output: {cm.output}") 

483 

484 logging.shutdown() 

485 

486 def testLogLoop(self): 

487 """Test that Python log forwarding works even if Python logging has 

488 been forwarded to lsst.log""" 

489 

490 log.configure() 

491 

492 # Note that assertLogs causes a specialists Python logging handler 

493 # to be added. 

494 

495 # Set up some Python loggers 

496 loggername = "testLogLoop" 

497 lgr = logging.getLogger(loggername) 

498 lgr.setLevel(logging.INFO) 

499 rootlgr = logging.getLogger() 

500 rootlgr.setLevel(logging.INFO) 

501 

502 # Declare that we are using the Python logger and that this will 

503 # not cause a log loop if we also are forwarding Python logging to 

504 # lsst.log 

505 log.usePythonLogging() 

506 

507 # Ensure that we can log both in lsst.log and Python 

508 rootlgr.addHandler(log.LogHandler()) 

509 

510 # All three of these messages go through LogHandler 

511 # The first two because they have the handler added explicitly, the 

512 # the final one because the lsst.log logger is forwarded to the 

513 # ROOT Python logger which has the LogHandler registered. 

514 

515 with open(self.outputFilename, "w") as fd: 

516 # Adding a StreamHandler will cause the LogHandler to no-op 

517 streamHandler = logging.StreamHandler(stream=fd) 

518 rootlgr.addHandler(streamHandler) 

519 

520 # Do not use assertLogs since that messes with handlers 

521 lgr.info("INFO message: Python child logger, lsst.log.LogHandler + PythonLogging") 

522 rootlgr.info("INFO message: Python root logger, lsst.log.logHandler + PythonLogging") 

523 

524 # This will use a ROOT python logger which has a LogHandler attached 

525 log.info("INFO message: lsst.log root logger, PythonLogging") 

526 

527 rootlgr.removeHandler(streamHandler) 

528 

529 self.check(""" 

530INFO message: Python child logger, lsst.log.LogHandler + PythonLogging 

531INFO message: Python root logger, lsst.log.logHandler + PythonLogging 

532INFO message: lsst.log root logger, PythonLogging""") 

533 

534 with open(self.outputFilename, "w") as fd: 

535 # Adding a StreamHandler will cause the LogHandler to no-op 

536 streamHandler = logging.StreamHandler(stream=fd) 

537 rootlgr.addHandler(streamHandler) 

538 

539 # Do not use assertLogs since that messes with handlers 

540 lgr.info("INFO message: Python child logger, lsst.log.LogHandler + PythonLogging") 

541 rootlgr.info("INFO message: Python root logger, lsst.log.logHandler + PythonLogging") 

542 

543 # This will use a ROOT python logger which has a LogHandler attached 

544 log.info("INFO message: lsst.log root logger, PythonLogging") 

545 

546 rootlgr.removeHandler(streamHandler) 

547 

548 self.check(""" 

549INFO message: Python child logger, lsst.log.LogHandler + PythonLogging 

550INFO message: Python root logger, lsst.log.logHandler + PythonLogging 

551INFO message: lsst.log root logger, PythonLogging""") 

552 

553 with self.assertLogs(level="INFO") as cm: 

554 rootlgr.info("Python log message forward to lsst.log") 

555 log.info("lsst.log message forwarded to Python") 

556 

557 self.assertEqual(len(cm.output), 2, f"Got output: {cm.output}") 

558 

559 logging.shutdown() 

560 

561 def testForwardToPythonContextManager(self): 

562 """Test that `lsst.log` log messages can be forwarded to `logging` 

563 using context manager""" 

564 log.configure() 

565 

566 # Without forwarding we only get python logger messages captured 

567 with self.assertLogs(level="WARNING") as cm: 

568 log.warning("lsst.log: not forwarded") 

569 logging.warning("Python logging: captured") 

570 self.assertEqual(len(cm.output), 1) 

571 

572 # Temporarily turn on forwarding 

573 with log.UsePythonLogging(): 

574 with self.assertLogs(level="WARNING") as cm: 

575 log.warn("lsst.log: forwarded") 

576 logging.warning("Python logging: also captured") 

577 self.assertEqual(len(cm.output), 2) 

578 

579 # Verify that forwarding is disabled 

580 self.assertFalse(log.Log.UsePythonLogging) 

581 

582 def testForwardToPythonAppender(self): 

583 """Test that `log4cxx` appender forwards it all to logging""" 

584 self.configure(""" 

585log4j.rootLogger=DEBUG, PyLog 

586log4j.appender.PyLog = PyLogAppender 

587""") 

588 with self.assertLogs(level="WARNING") as cm: 

589 log.warn("lsst.log: forwarded") 

590 logging.warning("Python logging: also captured") 

591 self.assertEqual(len(cm.output), 2) 

592 

593 # check that MDC is stored in LogRecord 

594 log.MDC("LABEL", "some.task") 

595 with self.assertLogs(level="WARNING") as cm: 

596 log.warn("lsst.log: forwarded") 

597 log.MDCRemove("LABEL") 

598 self.assertEqual(len(cm.records), 1) 

599 self.assertEqual(cm.records[0].MDC, {"LABEL": "some.task"}) 

600 self.assertEqual(cm.records[0].msg, "lsst.log: forwarded") 

601 

602 def testForwardToPythonAppenderWithMDC(self): 

603 """Test that `log4cxx` appender forwards it all to logging and modifies 

604 message with MDC info""" 

605 self.configure(""" 

606log4j.rootLogger=DEBUG, PyLog 

607log4j.appender.PyLog = PyLogAppender 

608log4j.appender.PyLog.MessagePattern = %m (LABEL=%X{{LABEL}}) 

609""") 

610 log.MDC("LABEL", "some.task") 

611 with self.assertLogs(level="WARNING") as cm: 

612 log.warn("lsst.log: forwarded") 

613 log.MDCRemove("LABEL") 

614 self.assertEqual(len(cm.records), 1) 

615 self.assertEqual(cm.records[0].MDC, {"LABEL": "some.task"}) 

616 self.assertEqual(cm.records[0].msg, "lsst.log: forwarded (LABEL=some.task)") 

617 

618 def testForwardToPythonAppenderFormatMDC(self): 

619 """Test that we can format `log4cxx` MDC on Python side""" 

620 

621 # remember old record factory 

622 old_factory = logging.getLogRecordFactory() 

623 

624 # configure things using convenience method 

625 log.configure_pylog_MDC("INFO") 

626 

627 with self.assertLogs(level="WARNING") as cm: 

628 log.warn("lsst.log: forwarded 1") 

629 log.MDC("LABEL", "task1") 

630 log.warn("lsst.log: forwarded 2") 

631 log.MDC("LABEL-X", "task2") 

632 log.warn("lsst.log: forwarded 3") 

633 logging.warning("Python logging: also captured") 

634 log.MDCRemove("LABEL") 

635 log.MDCRemove("LABEL-X") 

636 self.assertEqual(len(cm.records), 4) 

637 

638 # restore factory 

639 logging.setLogRecordFactory(old_factory) 

640 

641 # %-style formatting, only works on whole MDC 

642 formatter = logging.Formatter(fmt="%(levelname)s:%(name)s:%(message)s", style="%") 

643 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1") 

644 formatter = logging.Formatter(fmt="%(levelname)s:%(name)s:%(message)s:%(MDC)s", style="%") 

645 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:{}") 

646 self.assertEqual(formatter.format(cm.records[1]), 

647 "WARNING:root:lsst.log: forwarded 2:{LABEL=task1}") 

648 self.assertEqual(formatter.format(cm.records[2]), 

649 "WARNING:root:lsst.log: forwarded 3:{LABEL=task1, LABEL-X=task2}") 

650 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:{}") 

651 

652 # format-style formatting, without MDC first 

653 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}", style="{") 

654 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1") 

655 

656 # format-style formatting, with full MDC 

657 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}:{MDC}", style="{") 

658 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:{}") 

659 self.assertEqual(formatter.format(cm.records[1]), 

660 "WARNING:root:lsst.log: forwarded 2:{LABEL=task1}") 

661 self.assertEqual(formatter.format(cm.records[2]), 

662 "WARNING:root:lsst.log: forwarded 3:{LABEL=task1, LABEL-X=task2}") 

663 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:{}") 

664 

665 # format-style, using index access to MDC items, works for almost any 

666 # item names 

667 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}:{MDC[LABEL-X]}", style="{") 

668 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:") 

669 self.assertEqual(formatter.format(cm.records[1]), "WARNING:root:lsst.log: forwarded 2:") 

670 self.assertEqual(formatter.format(cm.records[2]), 

671 "WARNING:root:lsst.log: forwarded 3:task2") 

672 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:") 

673 

674 def testLevelTranslator(self): 

675 """Test LevelTranslator class 

676 """ 

677 # correspondence between levels, logging has no TRACE but we accept 

678 # small integer in its place 

679 levelMap = ((log.TRACE, 5), 

680 (log.DEBUG, logging.DEBUG), 

681 (log.INFO, logging.INFO), 

682 (log.WARN, logging.WARNING), 

683 (log.WARNING, logging.WARNING), 

684 (log.ERROR, logging.ERROR), 

685 (log.CRITICAL, logging.CRITICAL), 

686 (log.FATAL, logging.FATAL)) 

687 for logLevel, loggingLevel in levelMap: 

688 self.assertEqual(log.LevelTranslator.lsstLog2logging(logLevel), loggingLevel) 

689 self.assertEqual(log.LevelTranslator.logging2lsstLog(loggingLevel), logLevel) 

690 

691 def testLevelName(self): 

692 self.assertEqual(log.getLevelName(log.INFO), "INFO") 

693 self.assertEqual(log.getLevelName(3253), "Level 3253") 

694 

695 def testRepr(self): 

696 logger = log.getLogger("a.b") 

697 logger.setLevel(log.DEBUG) 

698 self.assertEqual(repr(logger), "<lsst.log.Log 'a.b' (DEBUG)>") 

699 

700 def testChildLogger(self): 

701 """Check the getChild logger method.""" 

702 logger = log.getDefaultLogger() 

703 self.assertEqual(logger.getName(), "") 

704 logger1 = logger.getChild("child1") 

705 self.assertEqual(logger1.getName(), "child1") 

706 logger2 = logger1.getChild("child2") 

707 self.assertEqual(logger2.getName(), "child1.child2") 

708 logger2a = logger1.getChild(".child2") 

709 self.assertEqual(logger2a.getName(), "child1.child2") 

710 logger3 = logger2.getChild(" .. child3") 

711 self.assertEqual(logger3.getName(), "child1.child2.child3") 

712 logger3a = logger1.getChild("child2.child3") 

713 self.assertEqual(logger3a.getName(), "child1.child2.child3") 

714 self.assertEqual(logger2.parent.name, logger1.name) 

715 self.assertEqual(logger1.parent.name, logger.name) 

716 self.assertIsNone(logger.parent) 

717 

718 

719if __name__ == "__main__": 719 ↛ 720line 719 didn't jump to line 720, because the condition on line 719 was never true

720 unittest.main()