a5ee482cee
Upstream python-systemd has a journald logging handler that also adds some exception information, as well as thread information. https://github.com/systemd/python-systemd/blob/master/systemd/journal.py#L581-L589 While OpenStack doesn't use a lot of threads, we might as well support it since it's no cost. Also, add PROCESS_NAME just to be compatible with other programs that might use python journald logging. Not entirely sure if record.processName and the results of our self.binary_name will be different. The exception info is the fun stuff though. Change-Id: Ibf0d7dae7587639737e0327f0338f30cdfc6aba1
162 lines
5.4 KiB
Python
162 lines
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import inspect
|
|
import logging
|
|
import logging.config
|
|
import logging.handlers
|
|
import os
|
|
import six
|
|
|
|
try:
|
|
from systemd import journal
|
|
except ImportError:
|
|
journal = None
|
|
try:
|
|
import syslog
|
|
except ImportError:
|
|
syslog = None
|
|
from oslo_utils import encodeutils
|
|
|
|
|
|
NullHandler = logging.NullHandler
|
|
|
|
|
|
def _get_binary_name():
|
|
return os.path.basename(inspect.stack()[-1][1])
|
|
|
|
|
|
_AUDIT = logging.INFO + 1
|
|
_TRACE = 5
|
|
|
|
# This is a copy of the numerical constants from syslog.h. The
|
|
# definition of these goes back at least 20 years, and is specifically
|
|
# 3 bits in a packed field, so these aren't likely to ever need
|
|
# changing.
|
|
SYSLOG_MAP = {
|
|
"CRITICAL": 2,
|
|
"ERROR": 3,
|
|
"WARNING": 4,
|
|
"WARN": 4,
|
|
"INFO": 6,
|
|
"DEBUG": 7,
|
|
}
|
|
|
|
|
|
class OSSysLogHandler(logging.Handler):
|
|
"""Syslog based handler. Only available on UNIX-like platforms."""
|
|
|
|
def __init__(self, facility=None):
|
|
# Default values always get evaluated, for which reason we avoid
|
|
# using 'syslog' directly, which may not be available.
|
|
facility = facility if facility is not None else syslog.LOG_USER
|
|
# Do not use super() unless type(logging.Handler) is 'type'
|
|
# (i.e. >= Python 2.7).
|
|
if not syslog:
|
|
raise RuntimeError("Syslog not available on this platform")
|
|
logging.Handler.__init__(self)
|
|
binary_name = _get_binary_name()
|
|
syslog.openlog(binary_name, 0, facility)
|
|
|
|
def emit(self, record):
|
|
priority = SYSLOG_MAP.get(record.levelname, 7)
|
|
message = self.format(record)
|
|
|
|
# NOTE(gangila): In python2, the syslog function takes in 's' as
|
|
# the format argument, which means it either accepts python string
|
|
# (str = 'a') or unicode strings (str = u'a'), the PyArg_ParseTuple
|
|
# then if needed converts the unicode objects to C strings using
|
|
# the *default encoding*. This default encoding is 'ascii' in case
|
|
# of python2 while it has been changed to 'utf-8' in case of
|
|
# python3. What this leads to is when we supply a syslog message
|
|
# like:
|
|
# >>> syslog.syslog(syslog.LOG_DEBUG, u"François Deppierraz")
|
|
# In case of python2 the above fails with TypeError: [priority,]
|
|
# message string. Because python2 doesn't explicitly encode as
|
|
# 'utf-8' and use the system default encoding ascii, which raises
|
|
# a UnicodeEncodeError (UnicodeEncodeError: 'ascii' codec can't
|
|
# encode character u'\xe7' in position 4: ordinal not in
|
|
# range(128)), and hence the error message that's set in the code
|
|
# (TypeError: [priority,] message string) gets shown to the user.
|
|
# However, this in the case of Python3, where the system default
|
|
# encoding is 'utf-8' works without any issues. Therefore, we need
|
|
# to safe_encode in case of python2 and not in case of Python3.
|
|
if six.PY2:
|
|
message = encodeutils.safe_encode(self.format(record))
|
|
|
|
syslog.syslog(priority, message)
|
|
|
|
|
|
class OSJournalHandler(logging.Handler):
|
|
|
|
custom_fields = (
|
|
'project_name',
|
|
'project_id',
|
|
'user_name',
|
|
'user_id',
|
|
'request_id',
|
|
)
|
|
|
|
def __init__(self):
|
|
# Do not use super() unless type(logging.Handler) is 'type'
|
|
# (i.e. >= Python 2.7).
|
|
if not journal:
|
|
raise RuntimeError("Systemd bindings do not exist")
|
|
logging.Handler.__init__(self)
|
|
self.binary_name = _get_binary_name()
|
|
|
|
def emit(self, record):
|
|
priority = SYSLOG_MAP.get(record.levelname, 7)
|
|
message = self.format(record)
|
|
|
|
extras = {
|
|
'CODE_FILE': record.pathname,
|
|
'CODE_LINE': record.lineno,
|
|
'CODE_FUNC': record.funcName,
|
|
'THREAD_NAME': record.threadName,
|
|
'PROCESS_NAME': record.processName,
|
|
'LOGGER_NAME': record.name,
|
|
'LOGGER_LEVEL': record.levelname,
|
|
'SYSLOG_IDENTIFIER': self.binary_name,
|
|
'PRIORITY': priority
|
|
}
|
|
|
|
if record.exc_text:
|
|
extras['EXCEPTION_TEXT'] = record.exc_text
|
|
|
|
if record.exc_info:
|
|
extras['EXCEPTION_INFO'] = record.exc_info
|
|
|
|
for field in self.custom_fields:
|
|
value = record.__dict__.get(field)
|
|
if value:
|
|
extras[field.upper()] = value
|
|
|
|
journal.send(message, **extras)
|
|
|
|
|
|
class ColorHandler(logging.StreamHandler):
|
|
LEVEL_COLORS = {
|
|
_TRACE: '\033[00;35m', # MAGENTA
|
|
logging.DEBUG: '\033[00;32m', # GREEN
|
|
logging.INFO: '\033[00;36m', # CYAN
|
|
_AUDIT: '\033[01;36m', # BOLD CYAN
|
|
logging.WARN: '\033[01;33m', # BOLD YELLOW
|
|
logging.ERROR: '\033[01;31m', # BOLD RED
|
|
logging.CRITICAL: '\033[01;31m', # BOLD RED
|
|
}
|
|
|
|
def format(self, record):
|
|
record.color = self.LEVEL_COLORS[record.levelno]
|
|
return logging.StreamHandler.format(self, record)
|