1# -*- coding: utf-8 -*-
2#    Licensed under the Apache License, Version 2.0 (the "License"); you may
3#    not use this file except in compliance with the License. You may obtain
4#    a copy of the License at
5#
6#         http://www.apache.org/licenses/LICENSE-2.0
7#
8#    Unless required by applicable law or agreed to in writing, software
9#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11#    License for the specific language governing permissions and limitations
12#    under the License.
13
14import inspect
15import logging
16import logging.config
17import logging.handlers
18import os
19
20try:
21    from systemd import journal
22except ImportError:
23    journal = None
24try:
25    import syslog
26except ImportError:
27    syslog = None
28
29
30NullHandler = logging.NullHandler
31
32
33def _get_binary_name():
34    return os.path.basename(inspect.stack()[-1][1])
35
36
37_AUDIT = logging.INFO + 1
38_TRACE = 5
39
40# This is a copy of the numerical constants from syslog.h. The
41# definition of these goes back at least 20 years, and is specifically
42# 3 bits in a packed field, so these aren't likely to ever need
43# changing.
44SYSLOG_MAP = {
45    "CRITICAL": 2,
46    "ERROR": 3,
47    "WARNING": 4,
48    "WARN": 4,
49    "INFO": 6,
50    "DEBUG": 7,
51}
52
53
54class OSSysLogHandler(logging.Handler):
55    """Syslog based handler. Only available on UNIX-like platforms."""
56
57    def __init__(self, facility=None):
58        # Default values always get evaluated, for which reason we avoid
59        # using 'syslog' directly, which may not be available.
60        facility = facility if facility is not None else syslog.LOG_USER
61        # Do not use super() unless type(logging.Handler) is 'type'
62        # (i.e. >= Python 2.7).
63        if not syslog:
64            raise RuntimeError("Syslog not available on this platform")
65        logging.Handler.__init__(self)
66        binary_name = _get_binary_name()
67        syslog.openlog(binary_name, 0, facility)
68
69    def emit(self, record):
70        priority = SYSLOG_MAP.get(record.levelname, 7)
71        message = self.format(record)
72        syslog.syslog(priority, message)
73
74
75class OSJournalHandler(logging.Handler):
76
77    custom_fields = (
78        'project_name',
79        'project_id',
80        'user_name',
81        'user_id',
82        'request_id',
83    )
84
85    def __init__(self, facility=None):
86        if not journal:
87            raise RuntimeError("Systemd bindings do not exist")
88
89        if not facility:
90            if not syslog:
91                raise RuntimeError("syslog is not available on this platform")
92            facility = syslog.LOG_USER
93
94        # Do not use super() unless type(logging.Handler) is 'type'
95        # (i.e. >= Python 2.7).
96        logging.Handler.__init__(self)
97        self.binary_name = _get_binary_name()
98        self.facility = facility
99
100    def emit(self, record):
101        priority = SYSLOG_MAP.get(record.levelname, 7)
102        message = self.format(record)
103
104        extras = {
105            'CODE_FILE': record.pathname,
106            'CODE_LINE': record.lineno,
107            'CODE_FUNC': record.funcName,
108            'THREAD_NAME': record.threadName,
109            'PROCESS_NAME': record.processName,
110            'LOGGER_NAME': record.name,
111            'LOGGER_LEVEL': record.levelname,
112            'SYSLOG_IDENTIFIER': self.binary_name,
113            'PRIORITY': priority,
114            'SYSLOG_FACILITY': self.facility,
115        }
116
117        if record.exc_info:
118            # Cache the traceback text to avoid converting it multiple times
119            # (it's constant anyway)
120            if not record.exc_text:
121                record.exc_text = self.formatter.formatException(
122                    record.exc_info)
123        if record.exc_text:
124            extras['EXCEPTION_INFO'] = record.exc_text
125            # Leave EXCEPTION_TEXT for backward compatibility
126            extras['EXCEPTION_TEXT'] = record.exc_text
127
128        for field in self.custom_fields:
129            value = record.__dict__.get(field)
130            if value:
131                extras[field.upper()] = value
132
133        journal.send(message, **extras)
134
135
136class ColorHandler(logging.StreamHandler):
137    """Log handler that sets the 'color' key based on the level
138
139    To use, include a '%(color)s' entry in the logging_context_format_string.
140    There is also a '%(reset_color)s' key that can be used to manually reset
141    the color within a log line.
142    """
143    LEVEL_COLORS = {
144        _TRACE: '\033[00;35m',  # MAGENTA
145        logging.DEBUG: '\033[00;32m',  # GREEN
146        logging.INFO: '\033[00;36m',  # CYAN
147        _AUDIT: '\033[01;36m',  # BOLD CYAN
148        logging.WARN: '\033[01;33m',  # BOLD YELLOW
149        logging.ERROR: '\033[01;31m',  # BOLD RED
150        logging.CRITICAL: '\033[01;31m',  # BOLD RED
151    }
152
153    def format(self, record):
154        record.color = self.LEVEL_COLORS[record.levelno]
155        record.reset_color = '\033[00m'
156        return logging.StreamHandler.format(self, record) + record.reset_color
157