first commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,238 @@
|
||||
import enum
|
||||
import io
|
||||
import time
|
||||
import traceback
|
||||
from string import Template
|
||||
from typing import Union, List
|
||||
from types import TracebackType
|
||||
|
||||
from aiologger.records import LogRecord, ExceptionInfo
|
||||
|
||||
|
||||
class FormatStyles(str, enum.Enum):
|
||||
PERCENT = "%"
|
||||
STRING_TEMPLATE = "$"
|
||||
STRING_FORMAT = "{"
|
||||
|
||||
|
||||
class PercentStyle:
|
||||
default_format = "%(message)s"
|
||||
asctime_format = "%(asctime)s"
|
||||
asctime_search = "%(asctime)"
|
||||
|
||||
def __init__(self, fmt: str = None) -> None:
|
||||
self._fmt = fmt or self.default_format
|
||||
self.uses_time = self._fmt.find(self.asctime_search) >= 0
|
||||
|
||||
def format(self, record: LogRecord) -> str:
|
||||
return self._fmt % record.__dict__
|
||||
|
||||
|
||||
class StrFormatStyle(PercentStyle):
|
||||
default_format = "{message}"
|
||||
asctime_format = "{asctime}"
|
||||
asctime_search = "{asctime"
|
||||
|
||||
def format(self, record: LogRecord) -> str:
|
||||
return self._fmt.format(**record.__dict__)
|
||||
|
||||
|
||||
class StringTemplateStyle(PercentStyle):
|
||||
default_format = "${message}"
|
||||
asctime_format = "${asctime}"
|
||||
asctime_search = "${asctime}"
|
||||
|
||||
def __init__(self, fmt: str = None) -> None:
|
||||
self._fmt = fmt or self.default_format
|
||||
self._template = Template(self._fmt)
|
||||
self.uses_time = (
|
||||
self._fmt.find("$asctime") >= 0
|
||||
or self._fmt.find(self.asctime_format) >= 0
|
||||
)
|
||||
|
||||
def format(self, record: LogRecord) -> str:
|
||||
return self._template.substitute(**record.__dict__)
|
||||
|
||||
|
||||
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
|
||||
|
||||
_STYLES = {
|
||||
"%": (PercentStyle, BASIC_FORMAT),
|
||||
"{": (StrFormatStyle, "{levelname}:{name}:{message}"),
|
||||
"$": (StringTemplateStyle, "${levelname}:${name}:${message}"),
|
||||
}
|
||||
|
||||
|
||||
class Formatter:
|
||||
"""
|
||||
Formatter instances are used to convert a ExtendedLogRecord to text.
|
||||
|
||||
Formatters need to know how a ExtendedLogRecord is constructed. They are
|
||||
responsible for converting a ExtendedLogRecord to (usually) a string which can
|
||||
be interpreted by either a human or an external system. The base Formatter
|
||||
allows a formatting string to be specified. If none is supplied, the
|
||||
default value of "%s(message)" is used.
|
||||
|
||||
The Formatter can be initialized with a format string which makes use of
|
||||
knowledge of the ExtendedLogRecord attributes - e.g. the default value mentioned
|
||||
above makes use of the fact that the user's message and arguments are pre-
|
||||
formatted into a ExtendedLogRecord's message attribute. Currently, the useful
|
||||
attributes in a ExtendedLogRecord are described by:
|
||||
|
||||
%(name)s Name of the logger (logging channel)
|
||||
%(levelno)s Numeric logging level for the message (DEBUG, INFO,
|
||||
WARNING, ERROR, CRITICAL)
|
||||
%(levelname)s Text logging level for the message ("DEBUG", "INFO",
|
||||
"WARNING", "ERROR", "CRITICAL")
|
||||
%(pathname)s Full pathname of the source file where the logging
|
||||
call was issued (if available)
|
||||
%(filename)s Filename portion of pathname
|
||||
%(module)s Module (name portion of filename)
|
||||
%(lineno)d Source line number where the logging call was issued
|
||||
(if available)
|
||||
%(funcName)s Function name
|
||||
%(created)f Time when the ExtendedLogRecord was created (time.time()
|
||||
return value)
|
||||
%(asctime)s Textual time when the ExtendedLogRecord was created
|
||||
%(msecs)d Millisecond portion of the creation time
|
||||
%(relativeCreated)d Time in milliseconds when the ExtendedLogRecord was created,
|
||||
relative to the time the logging module was loaded
|
||||
(typically at application startup time)
|
||||
%(thread)d Thread ID (if available)
|
||||
%(threadName)s Thread name (if available)
|
||||
%(process)d Process ID (if available)
|
||||
%(message)s The result of record.get_message(), computed just as
|
||||
the record is emitted
|
||||
"""
|
||||
|
||||
default_time_format = "%Y-%m-%d %H:%M:%S"
|
||||
default_msec_format = "%s,%03d"
|
||||
terminator = "\n"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fmt: str = None,
|
||||
datefmt: str = None,
|
||||
style: Union[str, FormatStyles] = "%",
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the formatter with specified format strings.
|
||||
|
||||
Initialize the formatter either with the specified format string, or a
|
||||
default as described above. Allow for specialized date formatting with
|
||||
the optional datefmt argument. If datefmt is omitted, you get an
|
||||
ISO8601-like (or RFC 3339-like) format.
|
||||
|
||||
Use a style parameter of '%', '{' or '$' to specify that you want to
|
||||
use one of %-formatting, :meth:`str.format` (``{}``) formatting or
|
||||
:class:`string.Template` formatting in your format string.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
Added the ``style`` parameter.
|
||||
"""
|
||||
if style not in _STYLES:
|
||||
valid_styles = ",".join(_STYLES.keys())
|
||||
raise ValueError(f"Style must be one of: {valid_styles}")
|
||||
|
||||
self._style = _STYLES[style][0](fmt)
|
||||
self._fmt = self._style._fmt
|
||||
self.datefmt = datefmt
|
||||
self.converter = time.localtime
|
||||
|
||||
def format_time(self, record: LogRecord, datefmt: str = None) -> str:
|
||||
"""
|
||||
Return the creation time of the specified ExtendedLogRecord as formatted text.
|
||||
|
||||
This method should be called from format() by a formatter which
|
||||
wants to make use of a formatted time. This method can be overridden
|
||||
in formatters to provide for any specific requirement, but the
|
||||
basic behaviour is as follows: if datefmt (a string) is specified,
|
||||
it is used with time.strftime() to format the creation time of the
|
||||
record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used.
|
||||
The resulting string is returned. This function uses a user-configurable
|
||||
function to convert the creation time to a tuple. By default,
|
||||
time.localtime() is used; to change this for a particular formatter
|
||||
instance, set the 'converter' attribute to a function with the same
|
||||
signature as time.localtime() or time.gmtime(). To change it for all
|
||||
formatters, for example if you want all logging times to be shown in GMT,
|
||||
set the 'converter' attribute in the Formatter class.
|
||||
"""
|
||||
ct = self.converter(record.created)
|
||||
if datefmt:
|
||||
return time.strftime(datefmt, ct)
|
||||
else:
|
||||
t = time.strftime(self.default_time_format, ct)
|
||||
return self.default_msec_format % (t, record.msecs)
|
||||
|
||||
def format_exception(self, exception_info: ExceptionInfo) -> str:
|
||||
"""
|
||||
Format and return the specified exception information as a string.
|
||||
|
||||
This default implementation just uses
|
||||
traceback.print_exception()
|
||||
"""
|
||||
string_io = io.StringIO()
|
||||
tb = exception_info[2]
|
||||
|
||||
traceback.print_exception(
|
||||
exception_info[0], exception_info[1], tb, None, string_io
|
||||
)
|
||||
|
||||
s = string_io.getvalue()
|
||||
string_io.close()
|
||||
if s[-1:] == self.terminator:
|
||||
s = s[:-1]
|
||||
return s
|
||||
|
||||
def format_message(self, record: LogRecord) -> str:
|
||||
return self._style.format(record)
|
||||
|
||||
def format_stack(self, stack_info):
|
||||
"""
|
||||
This method is provided as an extension point for specialized
|
||||
formatting of stack information.
|
||||
|
||||
The input data is a string as returned from a call to
|
||||
:func:`traceback.print_stack`, but with the last trailing newline
|
||||
removed.
|
||||
|
||||
The base implementation just returns the value passed in.
|
||||
"""
|
||||
return stack_info
|
||||
|
||||
@staticmethod
|
||||
def format_traceback(tb: TracebackType) -> List[str]:
|
||||
formatted_tb = "".join(traceback.format_tb(tb))
|
||||
return formatted_tb.strip().split("\n")
|
||||
|
||||
def format(self, record: LogRecord) -> str:
|
||||
"""
|
||||
Format the specified record as text.
|
||||
|
||||
The record's attribute dictionary is used as the operand to a
|
||||
string formatting operation which yields the returned string.
|
||||
Before formatting the dictionary, a couple of preparatory steps
|
||||
are carried out. The message attribute of the record is computed
|
||||
using LogRecord.get_message(). If the formatting string uses the
|
||||
time (as determined by a call to usesTime(), format_time() is
|
||||
called to format the event time. If there is exception information,
|
||||
it is formatted using format_exception() and appended to the message.
|
||||
"""
|
||||
record.message = record.get_message()
|
||||
if self._style.uses_time:
|
||||
record.asctime = self.format_time(record, self.datefmt)
|
||||
s = self.format_message(record)
|
||||
if record.exc_info:
|
||||
# Cache the traceback text to avoid converting it multiple times
|
||||
# (it's constant anyway)
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.format_exception(record.exc_info)
|
||||
if record.exc_text:
|
||||
if s[-1:] != self.terminator:
|
||||
s = s + self.terminator
|
||||
s = s + record.exc_text
|
||||
if record.stack_info:
|
||||
if s[-1:] != self.terminator:
|
||||
s = s + self.terminator
|
||||
s = s + self.format_stack(record.stack_info)
|
||||
return s
|
||||
@@ -0,0 +1,155 @@
|
||||
import json
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from inspect import istraceback
|
||||
from typing import Callable, Iterable, Union, Dict, Optional, List
|
||||
from datetime import timezone
|
||||
|
||||
from aiologger.formatters.base import Formatter
|
||||
from aiologger.levels import LEVEL_TO_NAME
|
||||
from aiologger.records import LogRecord
|
||||
from aiologger.utils import CallableWrapper
|
||||
|
||||
|
||||
LOGGED_AT_FIELDNAME = "logged_at"
|
||||
LINE_NUMBER_FIELDNAME = "line_number"
|
||||
FUNCTION_NAME_FIELDNAME = "function"
|
||||
LOG_LEVEL_FIELDNAME = "level"
|
||||
MSG_FIELDNAME = "msg"
|
||||
FILE_PATH_FIELDNAME = "file_path"
|
||||
|
||||
|
||||
class JsonFormatter(Formatter):
|
||||
def __init__(
|
||||
self,
|
||||
serializer: Callable[..., str] = json.dumps,
|
||||
default_msg_fieldname: str = None,
|
||||
) -> None:
|
||||
super(JsonFormatter, self).__init__()
|
||||
self.serializer = serializer
|
||||
self.default_msg_fieldname = default_msg_fieldname or MSG_FIELDNAME
|
||||
|
||||
def _default_handler(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif istraceback(obj):
|
||||
tb = "".join(traceback.format_tb(obj))
|
||||
return tb.strip().split("\n")
|
||||
elif isinstance(obj, Exception):
|
||||
return "Exception: %s" % repr(obj)
|
||||
elif type(obj) is type:
|
||||
return str(obj)
|
||||
elif isinstance(obj, CallableWrapper):
|
||||
return obj()
|
||||
return str(obj)
|
||||
|
||||
def format(self, record: LogRecord) -> str:
|
||||
"""
|
||||
Formats a record and serializes it as a JSON str. If record message isnt
|
||||
already a dict, initializes a new dict and uses `default_msg_fieldname`
|
||||
as a key as the record msg as the value.
|
||||
"""
|
||||
msg: Union[str, dict] = record.msg
|
||||
if not isinstance(msg, dict):
|
||||
msg = {self.default_msg_fieldname: msg}
|
||||
|
||||
if record.exc_info:
|
||||
msg["exc_info"] = record.exc_info
|
||||
if record.exc_text:
|
||||
msg["exc_text"] = record.exc_text
|
||||
|
||||
return self.serializer(msg, default=self._default_handler)
|
||||
|
||||
@classmethod
|
||||
def format_error_msg(cls, record: LogRecord, exception: Exception) -> Dict:
|
||||
traceback_info: Optional[List[str]]
|
||||
if exception.__traceback__:
|
||||
traceback_info = cls.format_traceback(exception.__traceback__)
|
||||
else:
|
||||
traceback_info = None
|
||||
return {
|
||||
"record": {
|
||||
LINE_NUMBER_FIELDNAME: record.lineno,
|
||||
LOG_LEVEL_FIELDNAME: record.levelname,
|
||||
FILE_PATH_FIELDNAME: record.filename,
|
||||
FUNCTION_NAME_FIELDNAME: record.funcName,
|
||||
MSG_FIELDNAME: str(record.msg),
|
||||
},
|
||||
LOGGED_AT_FIELDNAME: datetime.utcnow().isoformat(),
|
||||
"logger_exception": {
|
||||
"type": str(type(exception)),
|
||||
"exc": str(exception),
|
||||
"traceback": traceback_info,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ExtendedJsonFormatter(JsonFormatter):
|
||||
level_to_name_mapping = LEVEL_TO_NAME
|
||||
default_fields = frozenset(
|
||||
[
|
||||
LOG_LEVEL_FIELDNAME,
|
||||
LOGGED_AT_FIELDNAME,
|
||||
LINE_NUMBER_FIELDNAME,
|
||||
FUNCTION_NAME_FIELDNAME,
|
||||
FILE_PATH_FIELDNAME,
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
serializer: Callable[..., str] = json.dumps,
|
||||
default_msg_fieldname: str = None,
|
||||
exclude_fields: Iterable[str] = None,
|
||||
tz: timezone = None,
|
||||
) -> None:
|
||||
|
||||
super(ExtendedJsonFormatter, self).__init__(
|
||||
serializer=serializer, default_msg_fieldname=default_msg_fieldname
|
||||
)
|
||||
self.tz = tz
|
||||
if exclude_fields is None:
|
||||
self.log_fields = self.default_fields
|
||||
else:
|
||||
self.log_fields = self.default_fields - set(exclude_fields)
|
||||
|
||||
def formatter_fields_for_record(self, record: LogRecord):
|
||||
"""
|
||||
:type record: aiologger.records.ExtendedLogRecord
|
||||
"""
|
||||
datetime_serialized = (
|
||||
datetime.now(timezone.utc).astimezone(self.tz).isoformat()
|
||||
)
|
||||
|
||||
default_fields = (
|
||||
(LOGGED_AT_FIELDNAME, datetime_serialized),
|
||||
(LINE_NUMBER_FIELDNAME, record.lineno),
|
||||
(FUNCTION_NAME_FIELDNAME, record.funcName),
|
||||
(LOG_LEVEL_FIELDNAME, self.level_to_name_mapping[record.levelno]),
|
||||
(FILE_PATH_FIELDNAME, record.pathname),
|
||||
)
|
||||
|
||||
for field, value in default_fields:
|
||||
if field in self.log_fields:
|
||||
yield field, value
|
||||
|
||||
def format(self, record) -> str:
|
||||
"""
|
||||
:type record: aiologger.records.ExtendedLogRecord
|
||||
"""
|
||||
msg = dict(self.formatter_fields_for_record(record))
|
||||
if record.flatten and isinstance(record.msg, dict):
|
||||
msg.update(record.msg)
|
||||
else:
|
||||
msg[MSG_FIELDNAME] = record.msg
|
||||
|
||||
if record.extra:
|
||||
msg.update(record.extra)
|
||||
if record.exc_info:
|
||||
msg["exc_info"] = record.exc_info
|
||||
if record.exc_text:
|
||||
msg["exc_text"] = record.exc_text
|
||||
|
||||
return self.serializer(
|
||||
msg, default=self._default_handler, **record.serializer_kwargs
|
||||
)
|
||||
Reference in New Issue
Block a user