Completed
Pull Request — master (#2596)
by Edward
06:01
created

st2common.setup()   A

Complexity

Conditions 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 18
rs 9.4285
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.
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _srcfile was declared protected and should not be accessed from this context.

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:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
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)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _log was declared protected and should not be accessed from this context.

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:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
155
156
    def flush(self):
157
        pass
158
159
160
def _audit(logger, msg, *args, **kwargs):
161
    if logger.isEnabledFor(logging.AUDIT):
162
        logger._log(logging.AUDIT, msg, args, **kwargs)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _log was declared protected and should not be accessed from this context.

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:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
163
164
logging.Logger.audit = _audit
165
166
167
def _add_exclusion_filters(handlers, excludes=None):
168
    if excludes:
169
        for h in handlers:
170
            h.addFilter(ExclusionFilter(excludes))
171
172
173
def _redirect_stderr():
174
    # It is ok to redirect stderr as none of the st2 handlers write to stderr.
175
    sys.stderr = LoggingStream('STDERR')
176
177
178
def setup(config_file, redirect_stderr=True, excludes=None, disable_existing_loggers=False):
179
    """
180
    Configure logging from file.
181
    """
182
    try:
183
        logging.config.fileConfig(config_file,
184
                                  defaults=None,
185
                                  disable_existing_loggers=disable_existing_loggers)
186
        handlers = logging.getLoggerClass().manager.root.handlers
187
        _add_exclusion_filters(handlers=handlers, excludes=excludes)
188
        if redirect_stderr:
189
            _redirect_stderr()
190
    except Exception as exc:
191
        # revert stderr redirection since there is no logger in place.
192
        sys.stderr = sys.__stderr__
193
        # No logger yet therefore write to stderr
194
        sys.stderr.write('ERROR: %s' % traceback.format_exc())
195
        raise Exception(six.text_type(exc))
196