Zack Williams | 4113213 | 2018-10-19 12:14:45 -0700 | [diff] [blame] | 1 | # Copyright 2017-present Open Networking Foundation |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | import multistructlog |
| 16 | import os |
| 17 | import logging |
| 18 | import logging.config |
| 19 | import shutil |
| 20 | import json |
| 21 | import unittest |
| 22 | import structlog |
| 23 | |
| 24 | # test directory, cleared and left around after testing |
| 25 | test_scratch = "msl_test_scratch" |
| 26 | |
| 27 | |
| 28 | class TestMultiStructLog(unittest.TestCase): |
| 29 | |
| 30 | @classmethod |
| 31 | def setUpClass(clsG): |
| 32 | # delete and recreate test_scratch directory if it exists |
| 33 | if os.path.isdir(test_scratch): |
| 34 | shutil.rmtree(test_scratch) |
| 35 | |
| 36 | os.mkdir(test_scratch) |
| 37 | |
| 38 | def tearDown(self): |
| 39 | structlog.reset_defaults() |
| 40 | |
| 41 | def test_reload(self): |
| 42 | ''' |
| 43 | Test that creating creatinging multiple identical loggers will reuse |
| 44 | existing loggers |
| 45 | ''' |
| 46 | |
| 47 | logger0 = multistructlog.create_logger({'version': 1, 'foo': 'bar'}) |
| 48 | logger1 = multistructlog.create_logger({'version': 1, 'foo': 'bar'}) |
| 49 | logger2 = multistructlog.create_logger() |
| 50 | |
| 51 | self.assertEqual(logger0, logger1) |
| 52 | self.assertNotEqual(logger0, logger2) |
| 53 | |
| 54 | def test_different_formatters(self): |
| 55 | ''' |
| 56 | Test different formattters and levels to different output streams |
| 57 | NOTE: Only one test as logger has global state that is hard to reset |
| 58 | between tests without breaking other things. |
| 59 | ''' |
| 60 | |
| 61 | f1 = os.path.join(test_scratch, 'different_formatters_test_file1') |
| 62 | f2 = os.path.join(test_scratch, 'different_formatters_test_file2.json') |
| 63 | |
| 64 | logging_config = { |
| 65 | 'version': 1, |
| 66 | 'handlers': { |
| 67 | 'file1': { |
| 68 | 'class': 'logging.handlers.RotatingFileHandler', |
| 69 | 'level': 'WARNING', |
| 70 | 'formatter': 'structured', |
| 71 | 'filename': f1, |
| 72 | }, |
| 73 | 'file2': { |
| 74 | 'class': 'logging.handlers.RotatingFileHandler', |
| 75 | 'level': 'INFO', |
| 76 | 'formatter': 'json', |
| 77 | 'filename': f2, |
| 78 | }, |
| 79 | }, |
| 80 | 'formatters': { |
| 81 | 'json': { |
| 82 | '()': structlog.stdlib.ProcessorFormatter, |
| 83 | "processor": structlog.processors.JSONRenderer(), |
| 84 | }, |
| 85 | 'structured': { |
| 86 | '()': structlog.stdlib.ProcessorFormatter, |
| 87 | 'processor': structlog.dev.ConsoleRenderer(colors=False), |
| 88 | }, |
| 89 | }, |
| 90 | 'loggers': { |
| 91 | '': { |
| 92 | 'handlers': ['file1', 'file2'], |
| 93 | 'level': 'WARNING', |
| 94 | 'propagate': True |
| 95 | }, |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | # reset level to debug, overriding 'loggers' directive above |
| 100 | logger = multistructlog.create_logger(logging_config, |
| 101 | level=logging.DEBUG) |
| 102 | |
| 103 | extra_data = {'number': 42} |
| 104 | |
| 105 | logger.warning("should be in both files", extra=extra_data) |
| 106 | |
| 107 | # filtered by file1 handler |
| 108 | logger.info("should only be in file2") |
| 109 | |
| 110 | # filtered by both handlers, but not by loggers |
| 111 | logger.debug("should not be in either file") |
| 112 | |
| 113 | # test new trace level |
| 114 | logger.trace("testing trace, shouldn't be in either file") |
| 115 | |
| 116 | # check contents of file1 |
| 117 | with open(f1) as f1fh: |
Zack Williams | 940ff79 | 2018-11-02 11:11:20 -0700 | [diff] [blame] | 118 | |
| 119 | # regex at start should roughly match ISO8601 datetime |
| 120 | f1_desired = r"[\dTZ\-\.\:]+ \[warning \] should be in both files extra={'number': 42}" |
| 121 | self.assertRegexpMatches(f1fh.read(), f1_desired) |
Zack Williams | 4113213 | 2018-10-19 12:14:45 -0700 | [diff] [blame] | 122 | |
| 123 | # check contents of file2 |
| 124 | f2_read = [] |
| 125 | f2_desired = [ |
Zack Williams | 940ff79 | 2018-11-02 11:11:20 -0700 | [diff] [blame] | 126 | {"event": "should be in both files", "extra": {"number": 42}, |
| 127 | "level": "warning", "timestamp": "removed"}, |
| 128 | {"event": "should only be in file2", |
| 129 | "level": "info", "timestamp": "removed"}, |
Zack Williams | 4113213 | 2018-10-19 12:14:45 -0700 | [diff] [blame] | 130 | ] |
| 131 | |
| 132 | with open(f2) as f2fh: |
| 133 | for line in f2fh: |
Zack Williams | 940ff79 | 2018-11-02 11:11:20 -0700 | [diff] [blame] | 134 | jl = json.loads(line) |
| 135 | |
| 136 | # assert there is a timestamp, and remove it as it changes |
| 137 | self.assertTrue(True if "timestamp" in jl else False) |
| 138 | jl['timestamp'] = "removed" |
| 139 | |
| 140 | f2_read.append(jl) |
Zack Williams | 4113213 | 2018-10-19 12:14:45 -0700 | [diff] [blame] | 141 | |
| 142 | self.assertEqual(f2_read, f2_desired) |