blob: 960b5bf69316155de74c0c71b35fa72e936ae203 [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
Varun Belurf81a5fc2017-08-11 16:52:59 -070046PROCESSOR_MAP = {
47 'StreamHandler': structlog.dev.ConsoleRenderer(),
48 'LogstashHandler': structlog.processors.JSONRenderer(),
49}
50
Varun Belurf81a5fc2017-08-11 16:52:59 -070051class FormatterFactory:
52 def __init__(self, handler_name):
53 self.handler_name = handler_name
54
55 def __call__(self):
56 try:
57 processor = PROCESSOR_MAP[self.handler_name]
58 except KeyError:
59 processor = structlog.processors.KeyValueRenderer()
60
61 formatter = structlog.stdlib.ProcessorFormatter(processor)
62
63 return formatter
64
65
66class XOSLoggerFactory:
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040067 def __init__(self, handlers):
68 self.handlers = handlers
69
Varun Belurf81a5fc2017-08-11 16:52:59 -070070 def __call__(self):
71 base_logger = logging.getLogger()
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040072 base_logger.handlers = []
73 for h in self.handlers:
Varun Belurf81a5fc2017-08-11 16:52:59 -070074 formatter = FormatterFactory(h.__class__.__name__)()
75 h.setFormatter(formatter)
76 base_logger.addHandler(h)
77
78 self.logger = base_logger
79 return self.logger
80
81
82""" We expose the Structlog logging interface directly. This should allow callers to
83 bind contexts incrementally and configure and use other features of structlog directly
84
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040085 The use of structlog in Chameleon was used for reference when writing this code.
Varun Belurf81a5fc2017-08-11 16:52:59 -070086"""
87
Sapan Bhatia74cd1e42017-08-14 02:00:04 -040088CURRENT_LOGGER = None
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040089CURRENT_LOGGER_PARMS = None
Varun Belurf81a5fc2017-08-11 16:52:59 -070090
Sapan Bhatia9ec41c22017-08-24 05:31:28 -040091def create_logger(_config=None, extra_processors=[], force_create=False, level=None):
92 """
93 Args:
94 _config (dict): The standard config for Python's logging module
95 extra_processors(dict): Custom structlog processors
96 force_create(bool): Forces creation of the logger
97 level(logging.loglevel): Overrides logging level
98
99 Returns:
100 log: structlog logger
101 """
102
103 first_entry_elts = ['Starting']
Varun Belurf81a5fc2017-08-11 16:52:59 -0700104
105 """Inherit base options from config"""
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400106 if _config:
Sapan Bhatiae437cf42017-08-21 22:41:29 -0400107 logging_config = copy.deepcopy(_config)
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400108 else:
Varun Belurf81a5fc2017-08-11 16:52:59 -0700109 first_entry_elts.append('Config is empty')
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400110 logging_config = {'version': 1}
Varun Belurf81a5fc2017-08-11 16:52:59 -0700111
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400112 """Check if a logger with this configuration has already been created, if so, return that logger
113 instead of creating a new one"""
114 global CURRENT_LOGGER
115 global CURRENT_LOGGER_PARMS
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400116 if CURRENT_LOGGER and CURRENT_LOGGER_PARMS == (logging_config, extra_processors, level) and not force_create:
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400117 return CURRENT_LOGGER
118
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400119 if level:
120 try:
121 for k,v in logging_config['loggers'].iteritems():
122 v['level'] = level
123 except KeyError:
124 first_entry_elts.append('Level override failed')
Varun Belurf81a5fc2017-08-11 16:52:59 -0700125
Varun Belurf81a5fc2017-08-11 16:52:59 -0700126 logging.config.dictConfig(logging_config)
127
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400128 processors = copy.copy(extra_processors)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700129 processors.extend([
130 structlog.processors.StackInfoRenderer(),
131 structlog.processors.format_exc_info,
132 structlog.stdlib.ProcessorFormatter.wrap_for_formatter
133 ])
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400134
135 default_handlers = [
136 logging.StreamHandler(sys.stdout),
137 logstash.LogstashHandler('localhost', 5617, version=1)
138 ]
Varun Belurf81a5fc2017-08-11 16:52:59 -0700139
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400140 configured_handlers = logging.getLogger().handlers
141 handlers = configured_handlers if configured_handlers else default_handlers
142 factory = XOSLoggerFactory(handlers)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700143
144 structlog.configure(
145 processors=processors,
146 logger_factory=factory,
147 )
148
149 log = structlog.get_logger()
150 first_entry = '. '.join(first_entry_elts)
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400151 log.info(first_entry, level_override=level,**logging_config)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700152
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400153 CURRENT_LOGGER = log
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400154 CURRENT_LOGGER_PARMS = (logging_config, extra_processors, level)
Varun Belurf81a5fc2017-08-11 16:52:59 -0700155 return log
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400156
157if __name__ == '__main__':
Sapan Bhatia9ec41c22017-08-24 05:31:28 -0400158 l = create_logger({'version': 2, 'loggers':{'':{'level': 'INFO'}}}, level="INFO")
Sapan Bhatia74cd1e42017-08-14 02:00:04 -0400159 l.info("Test OK")