Source code for dockerflow.logging
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import json
import logging
import socket
import sys
import traceback
[docs]
class JsonLogFormatter(logging.Formatter):
"""Log formatter that outputs machine-readable json.
This log formatter outputs JSON format messages that are compatible with
Mozilla's standard heka-based log aggregation infrastructure.
.. seealso::
- https://wiki.mozilla.org/Firefox/Services/Logging
Adapted from:
https://github.com/mozilla-services/mozservices/blob/master/mozsvc/util.py#L106
"""
LOGGING_FORMAT_VERSION = "2.0"
# Map from Python logging to Syslog severity levels
SYSLOG_LEVEL_MAP = {
50: 2, # CRITICAL
40: 3, # ERROR
30: 4, # WARNING
20: 6, # INFO
10: 7, # DEBUG
}
# Syslog level to use when/if python level isn't found in map
DEFAULT_SYSLOG_LEVEL = 7
EXCLUDED_LOGRECORD_ATTRS = set(
(
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"message",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
)
)
def __init__(self, fmt=None, datefmt=None, style="%", logger_name="Dockerflow"):
parent_init = logging.Formatter.__init__
# The style argument was added in Python 3.1 and since
# the logging configuration via config (ini) files uses
# positional arguments we have to do a version check here
# to decide whether to pass the style argument or not.
if sys.version_info[:2] < (3, 1):
parent_init(self, fmt, datefmt)
else:
parent_init(self, fmt=fmt, datefmt=datefmt, style=style)
self.logger_name = logger_name
self.hostname = socket.gethostname()
[docs]
def is_value_jsonlike(self, value):
"""
Return True if the value looks like JSON. Use only on strings.
"""
return value.startswith("{") and value.endswith("}")
[docs]
def convert_record(self, record):
"""
Convert a Python LogRecord attribute into a dict that follows MozLog
application logging standard.
* from - https://docs.python.org/3/library/logging.html#logrecord-attributes
* to - https://wiki.mozilla.org/Firefox/Services/Logging
"""
out = {
"Timestamp": int(record.created * 1e9),
"Type": record.name,
"Logger": self.logger_name,
"Hostname": self.hostname,
"EnvVersion": self.LOGGING_FORMAT_VERSION,
"Severity": self.SYSLOG_LEVEL_MAP.get(
record.levelno, self.DEFAULT_SYSLOG_LEVEL
),
"Pid": record.process,
}
# Include any custom attributes set on the record.
# These would usually be collected metrics data.
fields = {}
for key, value in record.__dict__.items():
if key not in self.EXCLUDED_LOGRECORD_ATTRS:
fields[key] = value
# Only include the 'msg' key if it has useful content
# and is not already a JSON blob.
message = record.getMessage()
if message and not self.is_value_jsonlike(message):
fields["msg"] = message
# If there is an error, format it for nice output.
if record.exc_info:
fields["error"] = repr(record.exc_info[1])
fields["traceback"] = safer_format_traceback(*record.exc_info)
out["Fields"] = fields
return out
[docs]
def format(self, record):
"""
Format a Python LogRecord into a JSON string following MozLog
application logging standard.
"""
out = self.convert_record(record)
return json.dumps(out, cls=SafeJSONEncoder)
[docs]
def safer_format_traceback(exc_typ, exc_val, exc_tb):
"""Format an exception traceback into safer string.
We don't want to let users write arbitrary data into our logfiles,
which could happen if they e.g. managed to trigger a ValueError with
a carefully-crafted payload. This function formats the traceback
using "%r" for the actual exception data, which passes it through repr()
so that any special chars are safely escaped.
"""
lines = ["Uncaught exception:\n"]
lines.extend(traceback.format_tb(exc_tb))
lines.append("%r\n" % (exc_typ,))
lines.append("%r\n" % (exc_val,))
return "".join(lines)