blob: 8896cf221ffa5790fa272ce1e4ac243fc0f9bd13 [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
45
46
47PROCESSOR_MAP = {
48 'StreamHandler': structlog.dev.ConsoleRenderer(),
49 'LogstashHandler': structlog.processors.JSONRenderer(),
50}
51
52
53class FormatterFactory:
54 def __init__(self, handler_name):
55 self.handler_name = handler_name
56
57 def __call__(self):
58 try:
59 processor = PROCESSOR_MAP[self.handler_name]
60 except KeyError:
61 processor = structlog.processors.KeyValueRenderer()
62
63 formatter = structlog.stdlib.ProcessorFormatter(processor)
64
65 return formatter
66
67
68class XOSLoggerFactory:
69 def __init__(self, handlers):
70 self.handlers = handlers
71
72 def __call__(self):
73 base_logger = logging.getLogger()
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040074 base_logger.handlers = []
Varun Belurf81a5fc2017-08-11 16:52:59 -070075 for h in self.handlers:
76 formatter = FormatterFactory(h.__class__.__name__)()
77 h.setFormatter(formatter)
78 base_logger.addHandler(h)
79
80 self.logger = base_logger
81 return self.logger
82
83
84""" We expose the Structlog logging interface directly. This should allow callers to
85 bind contexts incrementally and configure and use other features of structlog directly
86
87 - config is the root xos configuration
88 - overrides override elements of that config, e.g. level=logging.INFO would cause debug messages to be dropped
89 - overrides can contain a 'processors' element, which lets you add processors to structlogs chain
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040090 - overrides can also contain force_create = True which returns a previously created logger. Multiple threads
91 will overwrite the shared logger.
Varun Belurf81a5fc2017-08-11 16:52:59 -070092
93 The use of structlog in Chameleon was used as a reference when writing this code.
94"""
95
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040096CURRENT_LOGGER = None
97CURRENT_LOGGER_PARMS = (None, None)
Varun Belurf81a5fc2017-08-11 16:52:59 -070098
99def create_logger(_config, **overrides):
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400100 first_entry_elts = []
Varun Belurf81a5fc2017-08-11 16:52:59 -0700101
102 """Inherit base options from config"""
103 try:
104 logging_config = copy.deepcopy(_config.get('logging'))
105 except AttributeError:
106 first_entry_elts.append('Config is empty')
107 logging_config = {}
108
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400109 """Check if a logger with this configuration has already been created, if so, return that logger
110 instead of creating a new one"""
111 global CURRENT_LOGGER
112 global CURRENT_LOGGER_PARMS
113
114 if CURRENT_LOGGER and CURRENT_LOGGER_PARMS == (logging_config, overrides) and not overrides.get('force_create'):
115 return CURRENT_LOGGER
116
117 first_entry_elts.append('Starting')
118 first_entry_struct = {}
119
Varun Belurf81a5fc2017-08-11 16:52:59 -0700120 if overrides:
121 first_entry_struct['overrides'] = overrides
122
123 for k, v in overrides.items():
124 logging_config[k] = v
125
126 default_handlers = [
127 logging.StreamHandler(sys.stdout),
128 logstash.LogstashHandler('localhost', 5617, version=1)
129 ]
130
131 handlers = logging_config.get('handlers', default_handlers)
132 logging.config.dictConfig(logging_config)
133
134 # Processors
135 processors = overrides.get('processors', [])
136
137 processors.extend([
138 structlog.processors.StackInfoRenderer(),
139 structlog.processors.format_exc_info,
140 structlog.stdlib.ProcessorFormatter.wrap_for_formatter
141 ])
142
143 factory = XOSLoggerFactory(handlers)
144
145 structlog.configure(
146 processors=processors,
147 logger_factory=factory,
148 )
149
150 log = structlog.get_logger()
151 first_entry = '. '.join(first_entry_elts)
152 log.info(first_entry, **first_entry_struct)
153
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400154 CURRENT_LOGGER = log
155 CURRENT_LOGGER_PARMS = (logging_config, overrides)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700156 return log
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400157
158if __name__ == '__main__':
159 l = create_logger({'logging': {'version': 2, 'loggers':{'':{'level': 'INFO'}}}}, level="INFO")
160 l.info("Test OK")