# From https://github.com/python/cpython/blob/master/Lib/logging/__init__.py
# https://github.com/python/cpython/commit/cb65b3a4f484ce71dcb76a918af98c7015513025
# (git master branch post 3.8)
# Copyright 2001-2019 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Vinay Sajip
# not be used in advertising or publicity pertaining to distribution
# of the software without specific, written prior permission.
import logging
import re
from string import Formatter as StrFormatter
log = logging.getLogger(__name__)
_str_formatter = StrFormatter()
[docs]class PercentStyle(object):
default_format = '%(message)s'
asctime_format = '%(asctime)s'
asctime_search = '%(asctime)'
validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I)
attrs_pattern_str = r'(?P<full_attr>%\((?P<attr_name>.*?)\).*?[dsf])'
attrs_pattern = re.compile(attrs_pattern_str)
named_fields_pattern = r'''(?P<full_attr>%\((?P<attr_name>.*?)\)(?P<conversion_flag>[#0+ -]*)''' + \
r'''(?P<field_width>\*|\d+)?(?P<precision>\.(\*|\d+))?(?P<conversion_type>[diouxefgcrsa%]))'''
def __init__(self, fmt):
self._fmt = fmt or self.default_format
self._base_fmt = self._fmt
self._color_fmt = None
self._format_attrs = self.find_format_attrs(self._base_fmt)
[docs] def context_color_format_string(self, format_string, format_attrs):
"""For extending a format string for :py:class:`logging.Formatter` to include attributes with color info.
ie::
'%(process)d %(threadName)s - %(msg)'
becomes::
'%(_cdl_process)s%(process)d%(_cdl_reset)s %(_cdl_threadName)%(threadName)s%(_cdl_reset)s - %(msg)'
"""
format_attrs = self.find_format_attrs(format_string)
color_attrs_string = '|'.join([x[1] for x in format_attrs])
# This looks for log format strings like '%(threadName)s' or '%(process)d, and replaces
# the format specifier with a version wrapped with log record color attributes.
#
# For ex, '%(threadName)s' -> %(_cdl_threadName)s%(threadName)s%(cdl_reset)
#
# When ColorFormatter.format() is called on a LogRecord, this is equilivent to
# calling 'log_format_string % log_record.__dict__', and uses the normal (but 'old school')
# string formatting (ie, %s style). 'threadName' is populated automatically by a logging.Logger() class on .log(),
# but the custom fields like %(_cdl_threadName)s need to be added to the log record
# we pass to ColorFormatter.format()
#
# ColorFormatter.format() actually does this itself, but those attributes could be set else
# via a logger, a log adapter, a logging Filter attached to the logger, a filter attached
# to a logging.Handler, or by a logging handler itself. Since our attributes are purely for
# formatting, we just do it in the ColorFormatter.format()
# https://docs.python.org/2/library/logging.html#logrecord-attributes
#
# THe captured groups 'full_attr' is the entire record attribute from the string, including
# string formatting/precsion, and padding info. ie '%(process)-10d' is a process pid right justified with
# a max length of 10 chars.
#
# The capture 'attr_name' is just the name of the attr, 'process' or 'message' or 'levelname' for ex. This
# is extract so it can be used in the name of the color debug logger attribute that will set the color info
# For '%(process)-10d', that would make 'process' the attr_name, and the color attribute '%(_cdl_process)'
# color_attrs_string is a sub regex of the attr names to be given color using the re alternate '|' notation.
re_string = r"(?P<full_attr>%\((?P<attr_name>" + color_attrs_string + r"?)\).*?[dsf])"
color_format_re = re.compile(re_string)
replacement = r"%(_cdl_\g<attr_name>)s\g<full_attr>%(_cdl_unset)s"
# likely needs thread locking
# exc_info_post = '%(exc_text_sep)s%(exc_text)s%(exc_text_sep)s'
# format_string = '%s%s' % (format_string, exc_info_post)
format_string2 = color_format_re.sub(replacement, format_string)
# set the default color at the begining of the format string and add a reset to the end
format_string = r"%(_cdl_default)s" + format_string2 + r"%(_cdl_reset)s"
return format_string
@property
def color_fmt(self):
if not self._color_fmt:
self._color_fmt = self.context_color_format_string(self._base_fmt, self._format_attrs)
return self._color_fmt
[docs] def usesTime(self):
return self._fmt.find(self.asctime_search) >= 0
[docs] def validate(self):
"""Validate the input format, ensure it matches the correct style"""
if not self.validation_pattern.search(self._fmt):
raise ValueError("Invalid format '%s' for '%s' style" % (self._fmt, self.default_format[0]))
def _format(self, record_context):
return self.color_fmt % record_context
# TODO: rename, and/or subclass the py3 classes
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
_STYLES = {
'%': (PercentStyle, BASIC_FORMAT),
'{': (StrFormatStyle, '{levelname}:{name}:{message}'),
}