1
|
|
|
# Licensed to the StackStorm, Inc ('StackStorm') under one or more |
2
|
|
|
# contributor license agreements. See the NOTICE file distributed with |
3
|
|
|
# this work for additional information regarding copyright ownership. |
4
|
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0 |
5
|
|
|
# (the "License"); you may not use this file except in compliance with |
6
|
|
|
# the License. You may obtain a copy of the License at |
7
|
|
|
# |
8
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
9
|
|
|
# |
10
|
|
|
# Unless required by applicable law or agreed to in writing, software |
11
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
12
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13
|
|
|
# See the License for the specific language governing permissions and |
14
|
|
|
# limitations under the License. |
15
|
|
|
|
16
|
|
|
from __future__ import absolute_import |
17
|
|
|
|
18
|
|
|
import os |
19
|
|
|
import sys |
20
|
|
|
import logging |
21
|
|
|
import logging.config |
22
|
|
|
import logging.handlers |
23
|
|
|
import traceback |
24
|
|
|
from functools import wraps |
25
|
|
|
|
26
|
|
|
import six |
27
|
|
|
|
28
|
|
|
from st2common.logging.filters import ExclusionFilter |
29
|
|
|
|
30
|
|
|
# Those are here for backward compatibility reasons |
31
|
|
|
from st2common.logging.handlers import FormatNamedFileHandler |
32
|
|
|
from st2common.logging.handlers import ConfigurableSyslogHandler |
33
|
|
|
from st2common.util.misc import prefix_dict_keys |
34
|
|
|
from st2common.util.misc import get_normalized_file_path |
35
|
|
|
|
36
|
|
|
__all__ = [ |
37
|
|
|
'getLogger', |
38
|
|
|
'setup', |
39
|
|
|
|
40
|
|
|
'FormatNamedFileHandler', |
41
|
|
|
'ConfigurableSyslogHandler', |
42
|
|
|
|
43
|
|
|
'LoggingStream' |
44
|
|
|
] |
45
|
|
|
|
46
|
|
|
logging.AUDIT = logging.CRITICAL + 10 |
47
|
|
|
logging.addLevelName(logging.AUDIT, 'AUDIT') |
48
|
|
|
|
49
|
|
|
LOGGER_KEYS = [ |
50
|
|
|
'debug', |
51
|
|
|
'info', |
52
|
|
|
'warning', |
53
|
|
|
'error', |
54
|
|
|
'critical', |
55
|
|
|
'exception', |
56
|
|
|
'log', |
57
|
|
|
|
58
|
|
|
'audit' |
59
|
|
|
] |
60
|
|
|
|
61
|
|
|
# Note: This attribute is used by "find_caller" so it can correctly exclude this file when looking |
62
|
|
|
# for the logger method caller frame. |
63
|
|
|
_srcfile = get_normalized_file_path(__file__) |
64
|
|
|
|
65
|
|
|
|
66
|
|
|
def find_caller(): |
67
|
|
|
""" |
68
|
|
|
Find the stack frame of the caller so that we can note the source file name, line number and |
69
|
|
|
function name. |
70
|
|
|
|
71
|
|
|
Note: This is based on logging/__init__.py:findCaller and modified so it takes into account |
72
|
|
|
this file - https://hg.python.org/cpython/file/2.7/Lib/logging/__init__.py#l1233 |
73
|
|
|
""" |
74
|
|
|
rv = '(unknown file)', 0, '(unknown function)' |
75
|
|
|
|
76
|
|
|
try: |
77
|
|
|
f = logging.currentframe().f_back |
78
|
|
|
while hasattr(f, 'f_code'): |
79
|
|
|
co = f.f_code |
80
|
|
|
filename = os.path.normcase(co.co_filename) |
81
|
|
|
if filename in (_srcfile, logging._srcfile): # This line is modified. |
|
|
|
|
82
|
|
|
f = f.f_back |
83
|
|
|
continue |
84
|
|
|
rv = (filename, f.f_lineno, co.co_name) |
85
|
|
|
break |
86
|
|
|
except Exception: |
87
|
|
|
pass |
88
|
|
|
|
89
|
|
|
return rv |
90
|
|
|
|
91
|
|
|
|
92
|
|
|
def decorate_log_method(func): |
93
|
|
|
@wraps(func) |
94
|
|
|
def func_wrapper(*args, **kwargs): |
95
|
|
|
# Prefix extra keys with underscore |
96
|
|
|
if 'extra' in kwargs: |
97
|
|
|
kwargs['extra'] = prefix_dict_keys(dictionary=kwargs['extra'], prefix='_') |
98
|
|
|
|
99
|
|
|
try: |
100
|
|
|
return func(*args, **kwargs) |
101
|
|
|
except TypeError as e: |
102
|
|
|
# In some version of Python 2.7, logger.exception doesn't take any kwargs so we need |
103
|
|
|
# this hack :/ |
104
|
|
|
# See: |
105
|
|
|
# - https://docs.python.org/release/2.7.3/library/logging.html#logging.Logger.exception |
106
|
|
|
# - https://docs.python.org/release/2.7.7/library/logging.html#logging.Logger.exception |
107
|
|
|
if 'got an unexpected keyword argument \'extra\'' in str(e): |
108
|
|
|
kwargs.pop('extra', None) |
109
|
|
|
return func(*args, **kwargs) |
110
|
|
|
raise e |
111
|
|
|
return func_wrapper |
112
|
|
|
|
113
|
|
|
|
114
|
|
|
def decorate_logger_methods(logger): |
115
|
|
|
""" |
116
|
|
|
Decorate all the logger methods so all the keys in the extra dictionary are |
117
|
|
|
automatically prefixed with an underscore to avoid clashes with standard log |
118
|
|
|
record attributes. |
119
|
|
|
""" |
120
|
|
|
|
121
|
|
|
# Note: We override findCaller with our custom implementation which takes into account this |
122
|
|
|
# module. |
123
|
|
|
# This way filename, module, funcName and lineno LogRecord attributes contain correct values |
124
|
|
|
# instead of all pointing to decorate_log_method. |
125
|
|
|
logger.findCaller = find_caller |
126
|
|
|
for key in LOGGER_KEYS: |
127
|
|
|
log_method = getattr(logger, key) |
128
|
|
|
log_method = decorate_log_method(log_method) |
129
|
|
|
setattr(logger, key, log_method) |
130
|
|
|
|
131
|
|
|
return logger |
132
|
|
|
|
133
|
|
|
|
134
|
|
|
def getLogger(name): |
135
|
|
|
# make sure that prefix isn't appended multiple times to preserve logging name hierarchy |
136
|
|
|
prefix = 'st2.' |
137
|
|
|
if name.startswith(prefix): |
138
|
|
|
logger = logging.getLogger(name) |
139
|
|
|
else: |
140
|
|
|
logger_name = '{}{}'.format(prefix, name) |
141
|
|
|
logger = logging.getLogger(logger_name) |
142
|
|
|
|
143
|
|
|
logger = decorate_logger_methods(logger=logger) |
144
|
|
|
return logger |
145
|
|
|
|
146
|
|
|
|
147
|
|
|
class LoggingStream(object): |
148
|
|
|
|
149
|
|
|
def __init__(self, name, level=logging.ERROR): |
150
|
|
|
self._logger = getLogger(name) |
151
|
|
|
self._level = level |
152
|
|
|
|
153
|
|
|
def write(self, message): |
154
|
|
|
self._logger._log(self._level, message, None) |
|
|
|
|
155
|
|
|
|
156
|
|
|
|
157
|
|
|
def _audit(logger, msg, *args, **kwargs): |
158
|
|
|
if logger.isEnabledFor(logging.AUDIT): |
159
|
|
|
logger._log(logging.AUDIT, msg, args, **kwargs) |
|
|
|
|
160
|
|
|
|
161
|
|
|
logging.Logger.audit = _audit |
162
|
|
|
|
163
|
|
|
|
164
|
|
|
def _add_exclusion_filters(handlers, excludes=None): |
165
|
|
|
if excludes: |
166
|
|
|
for h in handlers: |
167
|
|
|
h.addFilter(ExclusionFilter(excludes)) |
168
|
|
|
|
169
|
|
|
|
170
|
|
|
def _redirect_stderr(): |
171
|
|
|
# It is ok to redirect stderr as none of the st2 handlers write to stderr. |
172
|
|
|
sys.stderr = LoggingStream('STDERR') |
173
|
|
|
|
174
|
|
|
|
175
|
|
|
def setup(config_file, redirect_stderr=True, excludes=None, disable_existing_loggers=False): |
176
|
|
|
""" |
177
|
|
|
Configure logging from file. |
178
|
|
|
""" |
179
|
|
|
try: |
180
|
|
|
logging.config.fileConfig(config_file, |
181
|
|
|
defaults=None, |
182
|
|
|
disable_existing_loggers=disable_existing_loggers) |
183
|
|
|
handlers = logging.getLoggerClass().manager.root.handlers |
184
|
|
|
_add_exclusion_filters(handlers) |
185
|
|
|
if redirect_stderr: |
186
|
|
|
_redirect_stderr() |
187
|
|
|
except Exception as exc: |
188
|
|
|
# revert stderr redirection since there is no logger in place. |
189
|
|
|
sys.stderr = sys.__stderr__ |
190
|
|
|
# No logger yet therefore write to stderr |
191
|
|
|
sys.stderr.write('ERROR: %s' % traceback.format_exc()) |
192
|
|
|
raise Exception(six.text_type(exc)) |
193
|
|
|
|
Prefixing a member variable
_
is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class: