Passed
Pull Request — master (#452)
by Valessio Soares de
02:56
created

LogManager   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 80
Duplicated Lines 0 %

Test Coverage

Coverage 84%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 80
ccs 21
cts 25
cp 0.84
rs 10
wmc 11

6 Methods

Rating   Name   Duplication   Size   Complexity  
A add_handler() 0 15 2
A _catch_config_file_exception() 0 11 2
A load_config_file() 0 18 2
A _use_config_file() 0 9 2
A _set_debug_mode() 0 6 2
A add_stream_handler() 0 10 1
1
"""Handle logs displayed by Kytos SDN Platform."""
2 1
import inspect
3 1
import re
4 1
from configparser import RawConfigParser
5 1
from logging import Formatter, StreamHandler, config, getLogger
6
from pathlib import Path
7 1
8
__all__ = ('LogManager', 'NAppLog')
9 1
log = getLogger(__name__)
10 1
11
12
class LogManager:
13 1
    """Manage handlers for all loggers."""
14
15
    _PARSER = RawConfigParser()
16 1
    _DEFAULT_FMT = 'formatter_console'
17
18
    @classmethod
19
    def load_config_file(cls, config_file, debug='False'):
20
        """Load log configuration file.
21
22
        Check whether file exists and if there's an OSError, try removing
23 1
        syslog handler.
24
25
        Args:
26
           config_file (str, Path): Configuration file path.
27
        """
28
        if Path(config_file).exists():
29
            cls._PARSER.read(config_file)
30 1
            cls._set_debug_mode(debug)
31 1
            cls._use_config_file(config_file)
32 1
        else:
33
            log.warning('Log config file "%s" does not exist. Using default '
34 1
                        'Python logging configuration.',
35
                        config_file)
36 1
37 1
    @classmethod
38
    def _set_debug_mode(cls, debug=False):
39
        if debug is True:
40 1
            cls._PARSER.set('logger_root', 'level', 'DEBUG')
41
            cls._PARSER.set('logger_api_server', 'level', 'DEBUG')
42
            log.info('Setting log configuration with debug mode.')
43
44
    @classmethod
45
    def _use_config_file(cls, config_file):
46
        """Use parsed logging configuration."""
47
        try:
48
            config.fileConfig(cls._PARSER, disable_existing_loggers=False)
49
            log.info('Logging config file "%s" loaded successfully.',
50
                     config_file)
51
        except OSError:
52
            cls._catch_config_file_exception(config_file)
53
54
    @classmethod
55
    def _catch_config_file_exception(cls, config_file):
56 1
        """Try not using syslog handler (for when it is not installed)."""
57
        if 'handler_syslog' in cls._PARSER:
58 1
            log.warning('Failed to load "%s". Trying to disable syslog '
59 1
                        'handler.', config_file)
60 1
            cls._PARSER.remove_section('handler_syslog')
61
            cls._use_config_file(config_file)
62
        else:
63
            log.warning('Failed to load "%s". Using default Python '
64 1
                        'logging configuration.', config_file)
65
66
    @classmethod
67 1
    def add_stream_handler(cls, stream):
68
        """Output all logs to the given stream.
69
70
        Args:
71
            stream: Object that supports ``write()`` and ``flush()``.
72
        """
73
        handler = StreamHandler(stream)
74
        cls.add_handler(handler)
75
        return handler
76
77 1
    @classmethod
78 1
    def add_handler(cls, handler):
79 1
        """Add handler to loggers.
80 1
81 1
        Use formatter_console if it exists.
82
83
        Args:
84
            handler(Handler): Handle to be added.
85
        """
86
        if cls._PARSER.has_section(cls._DEFAULT_FMT):
87
            fmt_conf = cls._PARSER[cls._DEFAULT_FMT]
88
            fmt = Formatter(fmt_conf.get('format', None),
89
                            fmt_conf.get('datefmt', None))
90
            handler.setFormatter(fmt)
91
        getLogger().addHandler(handler)
92
93
94
class NAppLog:
95
    """High-level logger for NApp devs.
96
97
    From NApp dev's point of view:
98
    - No need for instantiation
99
    - Logger name is automatically assigned
100
101
    Redirect all calls to a logger with the correct name (NApp ID).
102
103
    The appropriate logger is a logging.Logger with NApp ID as its name. If no
104
    NApp is found, use the root logger.
105
106
    As any NApp can use this logger, its name is detected in every call by
107
    inspecting the caller's stack. If no NApp is found, use the root logger.
108
    """
109
110
    def __getattribute__(self, name):
111
        """Detect NApp ID and use its logger."""
112
        napp_id = _detect_napp_id()
113
        logger = getLogger(napp_id)
114
        return logger.__getattribute__(name)
115
116
117
#: Detect NApp ID from filename
118
NAPP_ID_RE = re.compile(r'.*napps/(.*?)/(.*?)/')
119
120
121
def _detect_napp_id():
122
    """Get the last called NApp in caller's stack.
123
124
    We use the last innermost NApp because a NApp *A* may call a NApp *B* and,
125
    when *B* uses the logger, the logger's name should be *B*.
126
127
    Returns:
128
        str: NApp ID.
129
        None: If no NApp is found in the caller's stack.
130
    """
131
    for frame in inspect.stack():
132
        if not frame.filename == __file__:
133
            match = NAPP_ID_RE.match(frame.filename)
134
            if match:
135
                return '/'.join(match.groups())
136
    return None
137