blob: ecfd499479d6decd0626a50ac3dd850928b182bd [file] [log] [blame]
Varun Belurf81a5fc2017-08-11 16:52:59 -07001# 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"""
16multistructlog logging module
17
18This module enables structured data to be logged to a single destination, or to
19multiple destinations simulataneously. The API consists of a single function:
20create_logger, which returns a structlog object. You can invoke it as follows:
21
22 log = logger.create_logger(xos_config, level=logging.INFO)
23 log.info('Entered function', name = '%s' % fn_name)
24
25The default handlers in XOS are the console and Logstash. You can override the
26handlers, structlog's processors, or anything else by adding keyword arguments
27to create_logger:
28
29 log = logger.create_logger(xos_config, level=logging.INFO,
30 handlers=[logging.StreamHandler(sys.stdout),
31 logstash.LogstashHandler('somehost', 5617, version=1)])
32
33Each handler depends on a specific renderer (e.g. Logstash needs JSON and
34stdout needs ConsoleRenderer) but a structlog instance can enchain only one
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040035renderer. For this reason, we apply renderers at the logging layer, as
Varun Belurf81a5fc2017-08-11 16:52:59 -070036logging formatters.
37"""
38
39import logging
40import logging.config
41import logstash
42import structlog
43import sys
44import copy
Sapan Bhatiab7347742017-09-22 09:41:56 -070045import inspect
Varun Belurf81a5fc2017-08-11 16:52:59 -070046
Varun Belurf81a5fc2017-08-11 16:52:59 -070047PROCESSOR_MAP = {
Sapan Bhatiaf188c082018-02-01 11:33:16 -050048 'StreamHandler': structlog.dev.ConsoleRenderer(colors=True, force_colors=True),
Sapan Bhatia5ec80d32017-10-23 11:59:58 -040049 'LogstashHandler': structlog.processors.JSONRenderer(),
50 'RotatingFileHandler': structlog.processors.JSONRenderer()
Varun Belurf81a5fc2017-08-11 16:52:59 -070051}
52
Sapan Bhatiab7347742017-09-22 09:41:56 -070053
Varun Belurf81a5fc2017-08-11 16:52:59 -070054class FormatterFactory:
55 def __init__(self, handler_name):
56 self.handler_name = handler_name
57
58 def __call__(self):
59 try:
60 processor = PROCESSOR_MAP[self.handler_name]
61 except KeyError:
62 processor = structlog.processors.KeyValueRenderer()
63
64 formatter = structlog.stdlib.ProcessorFormatter(processor)
65
66 return formatter
67
68
69class XOSLoggerFactory:
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040070 def __init__(self, handlers):
71 self.handlers = handlers
72
Varun Belurf81a5fc2017-08-11 16:52:59 -070073 def __call__(self):
Sapan Bhatia5ec80d32017-10-23 11:59:58 -040074 base_logger = logging.getLogger("multistructlog")
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040075 base_logger.handlers = []
76 for h in self.handlers:
Varun Belurf81a5fc2017-08-11 16:52:59 -070077 formatter = FormatterFactory(h.__class__.__name__)()
78 h.setFormatter(formatter)
79 base_logger.addHandler(h)
80
81 self.logger = base_logger
82 return self.logger
83
84
85""" We expose the Structlog logging interface directly. This should allow callers to
Sapan Bhatiab7347742017-09-22 09:41:56 -070086 bind contexts incrementally and configure and use other features of structlog directly
Varun Belurf81a5fc2017-08-11 16:52:59 -070087
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040088 The use of structlog in Chameleon was used for reference when writing this code.
Varun Belurf81a5fc2017-08-11 16:52:59 -070089"""
90
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040091CURRENT_LOGGER = None
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040092CURRENT_LOGGER_PARMS = None
Varun Belurf81a5fc2017-08-11 16:52:59 -070093
Sapan Bhatiab7347742017-09-22 09:41:56 -070094
95def create_logger(_config=None, extra_processors=[],
96 force_create=False, level=None):
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040097 """
98 Args:
99 _config (dict): The standard config for Python's logging module
100 extra_processors(dict): Custom structlog processors
Sapan Bhatiab7347742017-09-22 09:41:56 -0700101 force_create(bool): Forces creation of the logger
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400102 level(logging.loglevel): Overrides logging level
103
104 Returns:
105 log: structlog logger
106 """
107
108 first_entry_elts = ['Starting']
Varun Belurf81a5fc2017-08-11 16:52:59 -0700109
110 """Inherit base options from config"""
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400111 if _config:
Sapan Bhatiae437cf42017-08-21 22:41:29 -0400112 logging_config = copy.deepcopy(_config)
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400113 else:
Varun Belurf81a5fc2017-08-11 16:52:59 -0700114 first_entry_elts.append('Config is empty')
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400115 logging_config = {'version': 1}
Varun Belurf81a5fc2017-08-11 16:52:59 -0700116
Sapan Bhatiab7347742017-09-22 09:41:56 -0700117 """Check if a logger with this configuration has already been created, if so, return that logger
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400118 instead of creating a new one"""
119 global CURRENT_LOGGER
120 global CURRENT_LOGGER_PARMS
Sapan Bhatiab7347742017-09-22 09:41:56 -0700121 if CURRENT_LOGGER and CURRENT_LOGGER_PARMS == (
122 logging_config, extra_processors, level) and not force_create:
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400123 return CURRENT_LOGGER
Sapan Bhatiab7347742017-09-22 09:41:56 -0700124
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400125 if level:
126 try:
Sapan Bhatiab7347742017-09-22 09:41:56 -0700127 for k, v in logging_config['loggers'].iteritems():
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400128 v['level'] = level
129 except KeyError:
130 first_entry_elts.append('Level override failed')
Varun Belurf81a5fc2017-08-11 16:52:59 -0700131
Varun Belurf81a5fc2017-08-11 16:52:59 -0700132 logging.config.dictConfig(logging_config)
133
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400134 processors = copy.copy(extra_processors)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700135 processors.extend([
136 structlog.processors.StackInfoRenderer(),
137 structlog.processors.format_exc_info,
138 structlog.stdlib.ProcessorFormatter.wrap_for_formatter
139 ])
Sapan Bhatiab7347742017-09-22 09:41:56 -0700140
Sapan Bhatia3ec782d2017-10-31 14:05:58 -0400141
142 handlers = logging.getLogger('multistructlog').handlers
Sapan Bhatiab7347742017-09-22 09:41:56 -0700143
Sapan Bhatia3ec782d2017-10-31 14:05:58 -0400144 if not handlers:
145 caller = inspect.stack()[1]
Varun Belurf81a5fc2017-08-11 16:52:59 -0700146
Sapan Bhatia3ec782d2017-10-31 14:05:58 -0400147 try:
148 filename = inspect.getmodule(caller[0]).__name__
149 except AttributeError:
150 filename = '/tmp/multistructlog'
151
152 handlers = [
153 logging.StreamHandler(sys.stdout),
154 logging.handlers.RotatingFileHandler(
155 filename=filename,
156 maxBytes=10485760,
157 backupCount=1)
158 ]
159
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400160 factory = XOSLoggerFactory(handlers)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700161
162 structlog.configure(
163 processors=processors,
164 logger_factory=factory,
165 )
166
167 log = structlog.get_logger()
168 first_entry = '. '.join(first_entry_elts)
Sapan Bhatiab7347742017-09-22 09:41:56 -0700169 log.info(first_entry, level_override=level, **logging_config)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700170
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400171 CURRENT_LOGGER = log
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400172 CURRENT_LOGGER_PARMS = (logging_config, extra_processors, level)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700173 return log
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400174
Sapan Bhatiab7347742017-09-22 09:41:56 -0700175
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400176if __name__ == '__main__':
Sapan Bhatiab7347742017-09-22 09:41:56 -0700177 l = create_logger(
178 {'version': 2, 'loggers': {'': {'level': 'INFO'}}}, level="INFO")
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400179 l.info("Test OK")