diff --git a/README.md b/README.md index 7c8e5db..30ff6fc 100644 --- a/README.md +++ b/README.md @@ -1 +1,280 @@ -# jsonformatter \ No newline at end of file +# jsonformatter -- A formatter for python log json format + +**jsonformatter** is a json formatter for python log handler, you can use it easily output LogStash needed format or other custom json format. + +jsonformatter requires Python 3.X. + + + +## Installation + +jsonformatter is available on PyPI. +Use pip to install: + +```shell +$ pip install jsonformatter +``` +or: + +```shell +$ git clone https://github.com/MyColorfulDays/jsonformatter.git +$ cd jsonformatter +$ python setup.py install +``` + +## LogRecord Attributes +Offical url: https://docs.python.org/3/library/logging.html#logrecord-attributes + +Attribute name|Format|Description +-|-|- +args|You shouldn’t need to format this yourself.|The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when there is only one argument, and it is a dictionary). +asctime|%(asctime)s|Human-readable time when the LogRecord was created. By default this is of the form ‘2003-07-08 16:49:45,896’ (the numbers after the comma are millisecond portion of the time). +created|%(created)f|Time when the LogRecord was created (as returned by time.time()). +exc_info|You shouldn’t need to format this yourself.|Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. +filename|%(filename)s|Filename portion of pathname. +funcName|%(funcName)s|Name of function containing the logging call. +levelname|%(levelname)s|Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). +levelno|%(levelno)s|Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). +lineno|%(lineno)d|Source line number where the logging call was issued (if available). +message|%(message)s|The logged message, computed as msg % args. This is set when Formatter.format() is invoked. +module|%(module)s|Module (name portion of filename). +msecs|%(msecs)d|Millisecond portion of the time when the LogRecord was created. +msg|You shouldn’t need to format this yourself.|The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object (see Using arbitrary objects as messages). +name|%(name)s|Name of the logger used to log the call. +pathname|%(pathname)s|Full pathname of the source file where the logging call was issued (if available). +process|%(process)d|Process ID (if available). +processName|%(processName)s|Process name (if available). +relativeCreated|%(relativeCreated)d|Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded. +stack_info|You shouldn’t need to format this yourself.|Stack frame information (where available) from the bottom of the stack in the current thread, up to and including the stack frame of the logging call which resulted in the creation of this record. +thread|%(thread)d|Thread ID (if available). +threadName|%(threadName)s|Thread name (if available). + +## Basic Usage +### Case 1. config in python code +```python3 +import logging + +from jsonformatter import JsonFormatter + +# format can be json string, OrderedDict, dict. +# if format is dict and python version<3.7.0, the output order is same of sorted keys. +# the key needn't same as attribute name, can be whatever you like. +# the value can be `Attribute name` or `Format`(`Attribute name` will diplay `LogRecord.attribute`, `Format` will diplay `str(LogRecord.attribute)`). +STRING_FORMAT = '''{ + "Name": "name", + "Levelno": "levelno", + "Levelname": "levelname", + "Pathname": "pathname", + "Filename": "filename", + "Module": "module", + "Lineno": "lineno", + "FuncName": "funcName", + "Created": "created", + "Asctime": "asctime", + "Msecs": "msecs", + "RelativeCreated": "relativeCreated", + "Thread": "thread", + "ThreadName": "threadName", + "Process": "process", + "Message": "message" +}''' + + + +root = logging.getLogger() +root.setLevel(logging.INFO) + +formatter = JsonFormatter(STRING_FORMAT) + +sh = logging.StreamHandler() +sh.setFormatter(formatter) +sh.setLevel(logging.INFO) + +root.addHandler(sh) + +root.info("test %s format", 'string') +``` + +### Case 2. config from config file +config file: +```shell +$ cat logger_config.ini +[loggers] +keys=root + +[logger_root] +level=DEBUG +handlers=infohandler + + +############################################### + +[handlers] +keys=infohandler + +[handler_infohandler] +class=StreamHandler +level=INFO +formatter=form01 +args=(sys.stdout,) + +############################################### + +[formatters] +keys=form01 + +[formatter_form01] +class=jsonformatter.JsonFormatter +format={"Name": "name","Levelno": "levelno","Levelname": "levelname","Pathname": "pathname","Filename": "filename","Module": "module","Lineno": "lineno","FuncName": "funcName","Created": "created","Asctime": "asctime","Msecs": "msecs","RelativeCreated": "relativeCreated","Thread": "thread","ThreadName": "threadName","Process": "process","Message": "message"} +``` +python code: +```python3 +import logging +import os +from logging.config import fileConfig + +fileConfig(os.path.join(os.path.dirname(__file__), 'logger_config.ini')) +root = logging.getLogger('root') +root.info('test file config') + +``` + +## More Usage + +### Case 1. output multiple attributes in one key +```python3 +import logging + +from jsonformatter import JsonFormatter + +MULTI_ATTRIBUTES_FORMAT = '''{ + "multi attributes in one key": "%(name)s - %(levelno)s - %(levelname)s - %(pathname)s - %(filename)s - %(module)s - %(lineno)d - %(funcName)s - %(created)f - %(asctime)s - %(msecs)d - %(relativeCreated)d - %(thread)d - %(threadName)s - %(process)d - %(message)s" +} +''' + + +root = logging.getLogger() +root.setLevel(logging.INFO) + +formatter = JsonFormatter(MULTI_ATTRIBUTES_FORMAT) + +sh = logging.StreamHandler() +sh.setFormatter(formatter) + +sh.setLevel(logging.INFO) + +root.addHandler(sh) +root.info('test multi attributes in one key') +``` + +### Case 2. support `json.dumps` all optional parameters +```python3 +import logging + +from jsonformatter import JsonFormatter + +STRING_FORMAT = '''{ + "Name": "name", + "Levelno": "levelno", + "Levelname": "levelname", + "Pathname": "pathname", + "Filename": "filename", + "Module": "module", + "Lineno": "lineno", + "FuncName": "funcName", + "Created": "created", + "Asctime": "asctime", + "Msecs": "msecs", + "RelativeCreated": "relativeCreated", + "Thread": "thread", + "ThreadName": "threadName", + "Process": "process", + "Message": "message" +}''' + +root = logging.getLogger() +root.setLevel(logging.INFO) + + +formatter = JsonFormatter(STRING_FORMAT, indent=4, ensure_ascii=False) + +sh = logging.StreamHandler() +sh.setFormatter(formatter) + +sh.setLevel(logging.INFO) + +root.addHandler(sh) + +root.info('test json optional paramter: 中文') +``` + +### Case 3. add/replace `LogRecord`'s attribute value +```python3 +import datetime +import json +import logging +import random +from collections import OrderedDict + +from jsonformatter import JsonFormatter + +# the key will add/replace `LogRecord`'s attribute +# the value must be `callable` type and not support paramters, it returned value will as the value of LogRecord's attribute +RECORD_CUSTOM_ATTRS = { + # datetime.datetime type is not JSON serializable. + # solve it in three ways. + # 1. use `Format` %(asctme)s. + # 2. use `json.dumps` optional parameter `default`. + # 3. use `json.dumps` optional parameter `cls`. + 'asctime': lambda: datetime.datetime.today(), + 'user id': lambda: str(random.random())[2:10] +} + +RECORD_CUSTOM_FORMAT = OrderedDict([ + ("User id", "user id"), # new custom attrs + ("Name", "name"), + ("Levelno", "levelno"), + ("Levelname", "levelname"), + ("Pathname", "pathname"), + ("Filename", "filename"), + ("Module", "module"), + ("Lineno", "lineno"), + ("FuncName", "funcName"), + ("Created", "created"), + ("Asctime", "%(asctime)s"), # use `Format` to convert returned value to string. + ("Msecs", "msecs"), + ("RelativeCreated", "relativeCreated"), + ("Thread", "thread"), + ("ThreadName", "threadName"), + ("Process", "process"), + ("Message", "message") +]) + + +def DEFAULT_SOLUTION(o): + if not isinstance(o,(str, int, float, bool, type(None))): + return str(o) + else: + return o + +class CLS_SOLUTION(json.JSONEncoder): + def default(self, o): + if isinstance(o, datetime.datetime): + return o.isoformat() + + return json.JSONEncoder.default(self, o) + +root = logging.getLogger() +root.setLevel(logging.INFO) + +formatter = JsonFormatter(RECORD_CUSTOM_FORMAT, record_custom_attrs=RECORD_CUSTOM_ATTRS, default=DEFAULT_SOLUTION, cls=CLS_SOLUTION) + +sh = logging.StreamHandler() +sh.setFormatter(formatter) + +sh.setLevel(logging.INFO) + +root.addHandler(sh) +root.info('record custom attrs') +``` + diff --git a/jsonformatter/__init__.py b/jsonformatter/__init__.py new file mode 100644 index 0000000..f486c78 --- /dev/null +++ b/jsonformatter/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +File: jsonformatter.py +Author: Me +Email: yourname@email.com +Github: https://github.com/yourname +Description: jsonformatter.py +""" +from .jsonformatter import JsonFormatter + +__all__ = ['JsonFormatter'] diff --git a/jsonformatter/jsonformatter.py b/jsonformatter/jsonformatter.py new file mode 100644 index 0000000..37db7d7 --- /dev/null +++ b/jsonformatter/jsonformatter.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +File: jsonformatter.py +Author: Me +Email: yourname@email.com +Github: https://github.com/yourname +Description: jsonformatter.py +""" +import datetime +import json +import logging +import sys +import warnings + +# From python3.7, dict is in ordered,so do json package's load(s)/dump(s). +# https://docs.python.org/3.7/library/stdtypes.html#dict +# Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6. +if sys.version_info >= (3, 7): + dictionary = dict +else: + from collections import OrderedDict + dictionary = OrderedDict + + +class JsonFormatter(logging.Formatter): + """ + Formatter instances are used to convert a LogRecord to text. + + Formatters need to know how a LogRecord is constructed. They are + responsible for converting a LogRecord to (usually) a string which can + be interpreted by either a human or an external system. The base Formatter + allows a formatting string to be specified. If none is supplied, the + default value of "%s(message)" is used. + + The Formatter can be initialized with a format string which makes use of + knowledge of the LogRecord attributes - e.g. the default value mentioned + above makes use of the fact that the user's message and arguments are pre- + formatted into a LogRecord's message attribute. Currently, the useful + attributes in a LogRecord are described by: + + %(name)s Name of the logger (logging channel) + %(levelno)s Numeric logging level for the message (DEBUG, INFO, + WARNING, ERROR, CRITICAL) + %(levelname)s Text logging level for the message ("DEBUG", "INFO", + "WARNING", "ERROR", "CRITICAL") + %(pathname)s Full pathname of the source file where the logging + call was issued (if available) + %(filename)s Filename portion of pathname + %(module)s Module (name portion of filename) + %(lineno)d Source line number where the logging call was issued + (if available) + %(funcName)s Function name + %(created)f Time when the LogRecord was created (time.time() + return value) + %(asctime)s Textual time when the LogRecord was created + %(msecs)d Millisecond portion of the creation time + %(relativeCreated)d Time in milliseconds when the LogRecord was created, + relative to the time the logging module was loaded + (typically at application startup time) + %(thread)d Thread ID (if available) + %(threadName)s Thread name (if available) + %(process)d Process ID (if available) + %(message)s The result of record.getMessage(), computed just as + the record is emitted + """ + + def parseFmt(self, fmt): + if isinstance(fmt, str): + return json.loads(fmt, object_pairs_hook=dictionary) + elif isinstance(fmt, OrderedDict): + return fmt + elif isinstance(fmt, dict): + if sys.version_info < (3, 7): + warnings.warn("Your Python version is below 3.7.0, the key's order of dict may be different from the definition, Please Use `OrderedDict`.", UserWarning) + return dictionary([(k, fmt[k]) for k in sorted(fmt.keys())]) + else: + raise TypeError('fmt must be jsong sting, OrderedDcit or dict type.') + + def checkRecordCustomAttrs(self, record_custom_attrs): + if isinstance(record_custom_attrs, dict): + for attr, value in record_custom_attrs.items(): + if not callable(value): + raise TypeError('%s is not callable.' % value) + else: + raise TypeError('record_custom_attrs must be dict type.') + + def __init__(self, fmt=None, datefmt=None, style='%', record_custom_attrs=None, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw): + """ + If ``record_custom_attrs`` is not ``None``, it must be a ``dict`` type, the key of dict will be setted as LogRecord's attribute, the value of key must be a callable object and without parameters, it returned obj will be setted as attribute's value of LogRecord. + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``int``, ``float``, ``bool``, ``None``) will be skipped + instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value can contain non-ASCII + characters if they appear in strings contained in ``obj``. Otherwise, all + such characters are escaped in JSON strings. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If specified, ``separators`` should be an ``(item_separator, key_separator)`` + tuple. The default is ``(', ', ': ')`` if *indent* is ``None`` and + ``(',', ': ')`` otherwise. To get the most compact JSON representation, + you should specify ``(',', ':')`` to eliminate whitespace. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + If *sort_keys* is true (default: ``False``), then the output of + dictionaries will be sorted by key. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. + + """ + if record_custom_attrs: + self.checkRecordCustomAttrs(record_custom_attrs) + + logging.Formatter.__init__(self, fmt='', datefmt=datefmt, style=style) + + self.record_custom_attrs = record_custom_attrs + self._style._fmt = '' + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.cls = cls + self.indent = indent + self.separators = separators + self.default = default + self.sort_keys = sort_keys + self.kw = kw + + self.json_fmt = self.parseFmt(fmt) + + def setRecordMessage(self, record, msg, args): + if not isinstance(msg, (str, int, float, bool, type(None))): + record.message = str(msg) + else: + record.message = msg + + if args: + record.message = str(record.message) % args + + if record.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) + if record.exc_text: + record.message = str(record.message) + if record.message[-1:] != "\n": + record.message = record.message + "\n" + record.message = record.message + record.exc_text + if record.stack_info: + record.message = str(record.message) + if record.message[-1:] != "\n": + record.message = record.message + "\n" + record.message = record.message + self.formatStack(record.stack_info) + + def setRecordCustomAttrs(self, record): + if self.record_custom_attrs: + for k, v in self.record_custom_attrs.items(): + setattr(record, k, v()) + + def formatMessage(self, record): + return self._style.format(record) + + def format(self, record): + result = dictionary() + + _msg, _args = record.msg, record.args + record.msg, record.args = '', tuple() + + self.setRecordMessage(record, _msg, _args) + + record.asctime = self.formatTime(record, self.datefmt) + + if self.record_custom_attrs: + self.setRecordCustomAttrs(record) + + for k, v in self.json_fmt.items(): + self._style._fmt = v + result[k] = getattr(record, v, None) if v in record.__dict__ else self.formatMessage(record) + self._style._fmt = '' + + return json.dumps(result, skipkeys=self.skipkeys, ensure_ascii=self.ensure_ascii, check_circular=self.check_circular, allow_nan=self.allow_nan, cls=self.cls, indent=self.indent, separators=self.separators, default=self.default, sort_keys=self.sort_keys, **self.kw) + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..189c019 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +File: setup.py +Author: Me +Email: yourname@email.com +Github: https://github.com/yourname +Description: setup.py +""" + +from setuptools import setup + +setup( + name='jsonformatter', + version='0.1.2', + description=( + 'Python log in json format.' + ), + long_description=open('README.md', encoding='utf-8').read(), + long_description_content_type="text/markdown", + platforms=["all"], + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: Implementation :: CPython', + 'Topic :: Utilities', + ], + python_requires='>=3', + author='MyColorfulDays', + author_email='my_colorful_days@163.com', + url='https://github.com/MyColorfulDays/jsonformatter.git', + license='BSD License', + packages=['jsonformatter'] +) + diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/logger_config.ini b/test/logger_config.ini new file mode 100644 index 0000000..8a8b2ef --- /dev/null +++ b/test/logger_config.ini @@ -0,0 +1,28 @@ +[loggers] +keys=root + +[logger_root] +level=DEBUG +handlers=infohandler + + +############################################### + +[handlers] +keys=infohandler + +[handler_infohandler] +class=StreamHandler +level=INFO +formatter=form01 +args=(sys.stdout,) + +############################################### + +[formatters] +keys=form01 + +[formatter_form01] +class=jsonformatter.JsonFormatter +datefmt=%Y-%m-%d %H:%M:%S +format={"name": "name","levelno": "levelno","levelname": "levelname","pathname": "pathname","filename": "filename","module": "module","lineno": "lineno","funcName": "funcName","created": "created","asctime": "asctime","msecs": "msecs","relativeCreated": "relativeCreated","thread": "thread","threadName": "threadName","process": "process","message": "message"} diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..ece25f7 --- /dev/null +++ b/test/test.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +File: jsonformatter.py +Author: Me +Email: yourname@email.com +Github: https://github.com/yourname +Description: jsonformatter.py +""" +import datetime +import logging +import os +import random +import unittest +from collections import OrderedDict +from logging.config import fileConfig + + +if __file__ == 'test.py': + import sys + sys.path.insert(0, '..') + +from jsonformatter import JsonFormatter + +STRING_FORMAT = '''{ + "name": "name", + "levelno": "levelno", + "levelname": "levelname", + "pathname": "pathname", + "filename": "filename", + "module": "module", + "lineno": "lineno", + "funcName": "funcName", + "created": "created", + "asctime": "asctime", + "msecs": "msecs", + "relativeCreated": "relativeCreated", + "thread": "thread", + "threadName": "threadName", + "process": "process", + "message": "message" +}''' + + +DICT_FORMAT = { + "name": "name", + "levelno": "levelno", + "levelname": "levelname", + "pathname": "pathname", + "filename": "filename", + "module": "module", + "lineno": "lineno", + "funcName": "funcName", + "created": "created", + "asctime": "asctime", + "msecs": "msecs", + "relativeCreated": "relativeCreated", + "thread": "thread", + "threadName": "threadName", + "process": "process", + "message": "message" +} + +ORDERED_DICT_FORMAT = OrderedDict([ + ("name", "name"), + ("levelno", "levelno"), + ("levelname", "levelname"), + ("pathname", "pathname"), + ("filename", "filename"), + ("module", "module"), + ("lineno", "lineno"), + ("funcName", "funcName"), + ("created", "created"), + ("asctime", "asctime"), + ("msecs", "msecs"), + ("relativeCreated", "relativeCreated"), + ("thread", "thread"), + ("threadName", "threadName"), + ("process", "process"), + ("message", "message") +]) + + +RECORD_CUSTOM_ATTRS = { + 'asctime': lambda: datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S.%f'), + 'user id': lambda: str(random.random())[2:10] +} + + +RECORD_CUSTOM_FORMAT = OrderedDict([ + ("user id", "user id"), # new custom attrs + ("name", "name"), + ("levelno", "levelno"), + ("levelname", "levelname"), + ("pathname", "pathname"), + ("filename", "filename"), + ("module", "module"), + ("lineno", "lineno"), + ("funcName", "funcName"), + ("created", "created"), + ("asctime", "asctime"), # use custom format replace default. + ("msecs", "msecs"), + ("relativeCreated", "relativeCreated"), + ("thread", "thread"), + ("threadName", "threadName"), + ("process", "process"), + ("message", "message") +]) + + +MULTI_VALUE_FORMAT = OrderedDict([ + ("multi value", "%(name)s - %(levelno)s - %(levelname)s - %(pathname)s - %(filename)s - %(module)s - %(lineno)d - %(funcName)s - %(created)f - %(asctime)s - %(msecs)d - %(relativeCreated)d - %(thread)d - %(threadName)s - %(process)d - %(message)s") +]) + + +class JsonFormatterTest(unittest.TestCase): + + def test_string_config(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(STRING_FORMAT, datefmt) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + + root.info("test %s format", 'string') + + def test_dict_config(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(DICT_FORMAT, datefmt) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + + root.info("test dict format") + + def test_ordered_dict_config(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(ORDERED_DICT_FORMAT, datefmt) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + + root.info("test dict format") + + def test_log_exception(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = '%Y-%m-%d %H:%M:%S' + sh = logging.StreamHandler() + formatter = JsonFormatter(ORDERED_DICT_FORMAT, datefmt) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + try: + 1 / 0 + except Exception as e: + root.exception('test log exception') + + def test_record_custom_attrs(self): + + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(RECORD_CUSTOM_FORMAT, datefmt, record_custom_attrs=RECORD_CUSTOM_ATTRS) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + root.info('test record custom attrs') + + def test_multi_value_in_one_key(self): + + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(MULTI_VALUE_FORMAT, datefmt, record_custom_attrs=RECORD_CUSTOM_ATTRS) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + root.info('test multi value in one key') + + def test_indent(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(ORDERED_DICT_FORMAT, datefmt, indent=4) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + + root.info('test indent') + + def test_ensure_ascii_false(self): + root = logging.getLogger() + root.setLevel(logging.INFO) + + datefmt = None + sh = logging.StreamHandler() + formatter = JsonFormatter(ORDERED_DICT_FORMAT, datefmt, ensure_ascii=False) + sh.setFormatter(formatter) + + sh.setLevel(logging.INFO) + + root.addHandler(sh) + + root.info('test ensure ascii false: 中文') + + def test_file_config(self): + fileConfig(os.path.join(os.path.dirname(__file__), 'logger_config.ini')) + root = logging.getLogger('root') + root.info('test file config') + + def tearDown(self): + root = logging.getLogger() + # remove handlers + root.handlers = [] + + +if __name__ == '__main__': + unittest.main() +