123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- '''
- This library is provided to allow standard python logging
- to output log data as JSON formatted strings
- '''
- import logging
- import json
- import re
- import datetime
- import traceback
- from inspect import istraceback
- #Support order in python 2.7 and 3
- try:
- from collections import OrderedDict
- except ImportError:
- pass
- # skip natural LogRecord attributes
- # http://docs.python.org/library/logging.html#logrecord-attributes
- RESERVED_ATTRS = (
- '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')
- RESERVED_ATTR_HASH = dict(zip(RESERVED_ATTRS, RESERVED_ATTRS))
- def merge_record_extra(record, target, reserved=RESERVED_ATTR_HASH):
- """
- Merges extra attributes from LogRecord object into target dictionary
- :param record: logging.LogRecord
- :param target: dict to update
- :param reserved: dict or list with reserved keys to skip
- """
- for key, value in record.__dict__.items():
- #this allows to have numeric keys
- if (key not in reserved
- and not (hasattr(key, "startswith")
- and key.startswith('_'))):
- target[key] = value
- return target
- class JsonFormatter(logging.Formatter):
- """
- A custom formatter to format logging records as json strings.
- extra values will be formatted as str() if nor supported by
- json default encoder
- """
- def __init__(self, *args, **kwargs):
- """
- :param json_default: a function for encoding non-standard objects
- as outlined in http://docs.python.org/2/library/json.html
- :param json_encoder: optional custom encoder
- :param json_serializer: a :meth:`json.dumps`-compatible callable
- that will be used to serialize the log record.
- :param prefix: an optional string prefix added at the beginning of
- the formatted string
- """
- self.json_default = kwargs.pop("json_default", None)
- self.json_encoder = kwargs.pop("json_encoder", None)
- self.json_serializer = kwargs.pop("json_serializer", json.dumps)
- self.json_indent = kwargs.pop("json_indent", None)
- self.prefix = kwargs.pop("prefix", "")
- #super(JsonFormatter, self).__init__(*args, **kwargs)
- logging.Formatter.__init__(self, *args, **kwargs)
- if not self.json_encoder and not self.json_default:
- def _default_json_handler(obj):
- '''Prints dates in ISO format'''
- if isinstance(obj, (datetime.date, datetime.time)):
- return obj.isoformat()
- elif istraceback(obj):
- tb = ''.join(traceback.format_tb(obj))
- return tb.strip()
- elif isinstance(obj, Exception):
- return "Exception: %s" % str(obj)
- return str(obj)
- self.json_default = _default_json_handler
- self._required_fields = self.parse()
- self._skip_fields = dict(zip(self._required_fields,
- self._required_fields))
- self._skip_fields.update(RESERVED_ATTR_HASH)
- def parse(self):
- """
- Parses format string looking for substitutions
- This method is responsible for returning a list of fields (as strings)
- to include in all log messages.
- """
- standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
- return standard_formatters.findall(self._fmt)
- def add_fields(self, log_record, record, message_dict):
- """
- Override this method to implement custom logic for adding fields.
- """
- for field in self._required_fields:
- log_record[field] = record.__dict__.get(field)
- log_record.update(message_dict)
- merge_record_extra(record, log_record, reserved=self._skip_fields)
- def process_log_record(self, log_record):
- """
- Override this method to implement custom logic
- on the possibly ordered dictionary.
- """
- return log_record
- def jsonify_log_record(self, log_record):
- """Returns a json string of the log record."""
- return self.json_serializer(log_record,
- default=self.json_default,
- cls=self.json_encoder,
- indent=self.json_indent)
- def format(self, record):
- """Formats a log record and serializes to json"""
- message_dict = {}
- if isinstance(record.msg, dict):
- message_dict = record.msg
- record.message = None
- else:
- record.message = record.getMessage()
- # only format time if needed
- if "asctime" in self._required_fields:
- record.asctime = self.formatTime(record, self.datefmt)
- # Display formatted exception, but allow overriding it in the
- # user-supplied dict.
- if record.exc_info and not message_dict.get('exc_info'):
- message_dict['exc_info'] = self.formatException(record.exc_info)
- if not message_dict.get('exc_info') and record.exc_text:
- message_dict['exc_info'] = record.exc_text
- try:
- log_record = OrderedDict()
- except NameError:
- log_record = {}
- self.add_fields(log_record, record, message_dict)
- log_record = self.process_log_record(log_record)
- return "%s%s" % (self.prefix, self.jsonify_log_record(log_record))
|