Skip to content

Commit

Permalink
Add docstrings and refactor JSON logging
Browse files Browse the repository at this point in the history
  • Loading branch information
reweeden committed Jan 28, 2022
1 parent 5ed31c4 commit 61e8535
Showing 1 changed file with 42 additions and 15 deletions.
57 changes: 42 additions & 15 deletions rain_api_core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ class PercentPlaceholder():
The placeholder can be formatted with the % operator.
>>> p = PercentPlaceholder("message")
>>> assert p % {"message": "hello"} == "hello"
>>> p % {"message": "hello"}
'hello'
"""

__slots__ = ("name", )
Expand All @@ -97,8 +98,27 @@ def __mod__(self, args):


class JSONPercentStyle():
"""Format log records into a JSON object (dict, list) using percent formatting
The `fmt` dict will be searched for percent formatting strings. When a value in
the structure is a string that entirely matches a single substitution parameter,
it will be replaced entirely by a JSON object. When the string contains multiple
parameters, or contains other data, then the data will be substituted into the
string as normal.
Examples:
>>> style1 = JSONPercentStyle({"msg": "%(message)s"})
>>> record = logging.makeLogRecord({"message": [1, 2, 3]})
>>> style1.format(record)
{'msg': [1, 2, 3]}
>>> style2 = JSONPercentStyle({"msg": "The message is %(message)s"})
>>> record = logging.makeLogRecord({"message": [1, 2, 3]})
>>> style2.format(record)
{'msg': 'The message is [1, 2, 3]'}
"""

default_format = {"message": "%(message)s"}
asctime_search = '%(asctime)'
placeholder_pattern = re.compile(r"^%\((\w+)\)s$")
validation_pattern = re.compile(r'%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]', re.I)

Expand All @@ -119,8 +139,8 @@ def func(obj):
return _map_json_object(func, obj)

def _usesTime(self):
for val in _walk_json_values(self._fmt):
if isinstance(val, str) and self.asctime_search in val:
for val in _iter_json_values(self._fmt):
if isinstance(val, str) and "%(asctime)" in val:
return True
if isinstance(val, PercentPlaceholder) and val.name == "asctime":
return True
Expand All @@ -129,24 +149,31 @@ def _usesTime(self):
def usesTime(self):
return self._uses_time

def _format(self, record):
def _format(self, record: logging.LogRecord):
return _fmt_json_object(self._fmt, record.__dict__)

def format(self, record):
def format(self, record: logging.LogRecord):
try:
return self._format(record)
except KeyError as e:
raise ValueError('Formatting field not found in record: %s' % e)
raise ValueError(f"Formatting field not found in record: {e}")


class JSONFormatter(logging.Formatter):
"""Format log records as JSON objects"""

def __init__(self, fmt=None, datefmt: str = None):
# Changing the type from the base class
self._style: JSONPercentStyle = JSONPercentStyle(fmt)
self._json_style = JSONPercentStyle(fmt)

self._fmt = self._style._fmt
self._fmt = self._json_style._fmt
self.datefmt = datefmt

def usesTime(self) -> bool:
return self._json_style.usesTime()

def formatMessage(self, record: logging.LogRecord):
return self._json_style.format(record)

def format(self, record: logging.LogRecord) -> str:
# Perform substitutions on the record itself
record.message = _fmt_json_object(record.msg, record.args)
Expand All @@ -156,7 +183,7 @@ def format(self, record: logging.LogRecord) -> str:
record.exc_obj = self.formatException(record.exc_info).split("\n") if record.exc_info else None

obj = self.formatMessage(record)
assert not any(isinstance(val, PercentPlaceholder) for val in _walk_json_values(obj))
assert not any(isinstance(val, PercentPlaceholder) for val in _iter_json_values(obj))

return filter_log_credentials(json.dumps(obj, default=str))

Expand Down Expand Up @@ -190,7 +217,7 @@ def log_context(**context):
custom_log_filter.update(**context)


def filter_log_credentials(msg):
def filter_log_credentials(msg: str):
if UNCENSORED_LOGGING:
return msg

Expand Down Expand Up @@ -223,12 +250,12 @@ def _map_json_object(func, obj):
return func(obj)


def _walk_json_values(obj):
def _iter_json_values(obj):
if isinstance(obj, dict):
for val in obj.values():
yield from _walk_json_values(val)
yield from _iter_json_values(val)
elif isinstance(obj, list):
for val in obj:
yield from _walk_json_values(val)
yield from _iter_json_values(val)
else:
yield obj

0 comments on commit 61e8535

Please sign in to comment.