diff --git a/example1.py b/example1.py index ad4d8e9..47f8e8b 100644 --- a/example1.py +++ b/example1.py @@ -23,3 +23,15 @@ 'test_list': [1, 2, '3'], } test_logger.info('python-logstash: test extra fields', extra=extra) + +class SomeClass: + + def __init__(self): + self.a = 1 + self.b = 1 + + def __repr__(self): + return "Test(a={}, b={})".format(self.a, self.b) + +test_logger.info("test inner json: %s", SomeClass(), extra={"meta": {"test": SomeClass()}}) + diff --git a/logstash/formatter.py b/logstash/formatter.py index f5c4424..438ce8a 100644 --- a/logstash/formatter.py +++ b/logstash/formatter.py @@ -3,6 +3,9 @@ import socket import sys from datetime import datetime +import six +from decimal import Decimal + try: import json except ImportError: @@ -10,6 +13,13 @@ class LogstashFormatterBase(logging.Formatter): + skip_list = ( + 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', + 'funcName', 'id', 'levelname', 'levelno', 'lineno', 'module', + 'msecs', 'msecs', 'message', 'msg', 'name', 'pathname', 'process', + 'processName', 'relativeCreated', 'thread', 'threadName', 'extra', + 'auth_token', 'password' + ) def __init__(self, message_type='Logstash', tags=None, fqdn=False): self.message_type = message_type @@ -20,29 +30,41 @@ def __init__(self, message_type='Logstash', tags=None, fqdn=False): else: self.host = socket.gethostname() + def _serialize_value(self, value): + # Recursively serializes value + + if isinstance(value, dict): + return { + key: self._serialize_value(item) + for key, item in six.iteritems(value) + } + elif isinstance(value, (set, tuple, list, frozenset)): + return [ + self._serialize_value(item) + for item in value + ] + else: + if sys.version_info < (3, 0): + easy_types = (basestring, bool, dict, float, int, long, list, type(None)) + else: + easy_types = (str, bool, dict, float, int, list, type(None)) + + if isinstance(value, easy_types): + return value + elif isinstance(value, Decimal): + return float(value) + else: + return repr(value) + def get_extra_fields(self, record): # The list contains all the attributes listed in # http://docs.python.org/library/logging.html#logrecord-attributes - skip_list = ( - 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', - 'funcName', 'id', 'levelname', 'levelno', 'lineno', 'module', - 'msecs', 'msecs', 'message', 'msg', 'name', 'pathname', 'process', - 'processName', 'relativeCreated', 'thread', 'threadName', 'extra', - 'auth_token', 'password') - - if sys.version_info < (3, 0): - easy_types = (basestring, bool, dict, float, int, long, list, type(None)) - else: - easy_types = (str, bool, dict, float, int, list, type(None)) fields = {} for key, value in record.__dict__.items(): - if key not in skip_list: - if isinstance(value, easy_types): - fields[key] = value - else: - fields[key] = repr(value) + if key not in self.skip_list: + fields[key] = self._serialize_value(value) return fields @@ -140,3 +162,4 @@ def format(self, record): message.update(self.get_debug_fields(record)) return self.serialize(message) + diff --git a/logstash/handler_tcp.py b/logstash/handler_tcp.py index cc687bd..07e2ceb 100644 --- a/logstash/handler_tcp.py +++ b/logstash/handler_tcp.py @@ -1,9 +1,12 @@ from logging.handlers import DatagramHandler, SocketHandler -from logstash import formatter # Derive from object to force a new-style class and thus allow super() to work # on Python 2.6 +from logstash import LogstashFormatterVersion0 +from logstash import LogstashFormatterVersion1 + + class TCPLogstashHandler(SocketHandler, object): """Python logging handler for Logstash. Sends events over TCP. :param host: The host of the logstash server. @@ -12,14 +15,21 @@ class TCPLogstashHandler(SocketHandler, object): :param fqdn; Indicates whether to show fully qualified domain name or not (default False). :param version: version of logstash event schema (default is 0). :param tags: list of tags for a logger (default is None). + :param fmt: custom formatter instance """ - def __init__(self, host, port=5959, message_type='logstash', tags=None, fqdn=False, version=0): + def __init__(self, host, port=5959, message_type='logstash', tags=None, fqdn=False, version=0, + formatterCls=None): super(TCPLogstashHandler, self).__init__(host, port) - if version == 1: - self.formatter = formatter.LogstashFormatterVersion1(message_type, tags, fqdn) + + if not formatterCls: + if version == 1: + self.formatter = LogstashFormatterVersion1(message_type, tags, fqdn) + else: + self.formatter = LogstashFormatterVersion0(message_type, tags, fqdn) else: - self.formatter = formatter.LogstashFormatterVersion0(message_type, tags, fqdn) + self.formatter = formatterCls(message_type, tags, fqdn) + def makePickle(self, record): return self.formatter.format(record) + b'\n' diff --git a/setup.py b/setup.py index 3329e91..a58aa18 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,10 @@ -from distutils.core import setup +from setuptools import setup + setup( name='python-logstash', packages=['logstash'], version='0.4.7', + install_requires=["six"], description='Python logging handler for Logstash.', long_description=open('README.rst').read(), license='MIT',