Source code for porter.utils
import datetime
import io
import json
import logging
import traceback
from inspect import istraceback
import numpy as np
[docs]
class NumpyEncoder(json.JSONEncoder):
"""A JSON encoder that handles ``numpy`` data types."""
[docs]
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
return super().default(obj)
[docs]
class PythonEncoder(json.JSONEncoder):
"""
A JSON encoder that extends ``json.JSONEncoder`` to handle additional Python
types.
"""
[docs]
def default(self, obj):
if isinstance(obj, (datetime.date, datetime.datetime, datetime.time)):
return obj.isoformat()
elif isinstance(obj, set):
return list(obj)
elif istraceback(obj):
with io.StringIO() as string_io:
traceback.print_tb(obj, file=string_io)
return string_io.getvalue()
elif isinstance(obj, Exception):
return repr(obj)
return super().default(obj)
[docs]
class AppEncoder(NumpyEncoder, PythonEncoder):
"""A JSON encoder that handles ``numpy`` and python data types."""
[docs]
def default(self, obj):
try:
return super().default(obj)
except TypeError:
try:
return str(obj)
except Exception:
pass
[docs]
class JSONLogFormatter(logging.Formatter):
"""A JSON formatter for logs.
Usage:
>>> logger = logging.getLogger(__name__)
>>> logger.setLevel('INFO')
>>> console = logging.StreamHandler()
>>> formatter = JSONLogFormatter('asctime', 'message', 'levelname')
>>> console.setFormatter(formatter)
>>> logger.addHandler(console)
>>> logger.info({'something': 'interesting'})
{"message": {"something": "interesting"}, "levelname": "INFO",
"asctime": "2018-07-05 11:48:54,248"}
Any attribute of ``logging.LogRecord`` can be specified to log. See
https://docs.python.org/3/library/logging.html#logging.LogRecord
Exception information does not need to be specified. If there was an
exception, that information is added automatically to the log.
>>> try:
>>> raise Exception('something bad')
>>> except Exception as err:
>>> logger.exception(err)
{"exc_text": "...", "exc_info": "...", "levelname": "ERROR",
"asctime": "2018-07-05 11:51:26,179", "message": "something bad"}
Args:
fields (list of str): List of fields to include in log. This can be
any attribute of a ``logging.LogRecord`` object plus "asctime" and
"message". For a list of ``logging.LogRecord`` attributes.
indent (int or None): The indentation level. Values are the same as
``json.dump``.
encoder (object): A ``json.JSONEncoder`` subclass. It is recommended to
use ``Encoder`` or a subclass thereof if you need additional
handling so that ``Exception``'s and ``datetime``'s are properly
handled.
**kwargs: Additional keyword arguments to be passed to
``logging.Formatter.__init__``.
"""
# tests can override this value as a `tuple` to preserve order.
_field_lookup_type = set
def __init__(self, *fields, indent=None, encoder=None, **kwargs):
self.fields = self._field_lookup_type(fields)
self.encoder = AppEncoder if encoder is None else encoder
self.indent = indent
super().__init__(**kwargs)
[docs]
def format(self, record):
record = self._process_record(record)
return json.dumps(record, cls=self.encoder, indent=self.indent)
def _process_record(self, record):
# following python implementation
# https://github.com/python/cpython/blob/f55a818954212e8e6c97e3d66cf1478120a3220f/Lib/logging/__init__.py#L563
fields = self._field_lookup_type(self.fields)
if 'message' in fields:
# skip default implementation which converts `record.msg` to a
# `dict` and then performs string formatting when `record.msg` is
# a `dict`.
if isinstance(record.msg, dict):
record.message = record.msg
else:
record.message = record.getMessage()
if 'asctime' in fields:
record.asctime = self.formatTime(record, self.datefmt)
if record.exc_info:
fields.add('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.formatException(record.exc_info)
fields.add('exc_text')
if record.stack_info:
fields.add('stack_info')
return {field: getattr(record, field, None) for field in fields}
[docs]
def object_constants(obj):
return [(attr, val) for attr, val in vars(obj).items()
if not attr.startswith('_') and attr.isupper()]
# keep this reference for backwards compatibility
JSONFormatter = JSONLogFormatter