pax_global_header00006660000000000000000000000064147601637340014525gustar00rootroot0000000000000052 comment=7b15ab338aea4738c34f786b5ee737be04b82670 json-log-formatter-1.1.1/000077500000000000000000000000001476016373400152565ustar00rootroot00000000000000json-log-formatter-1.1.1/.github/000077500000000000000000000000001476016373400166165ustar00rootroot00000000000000json-log-formatter-1.1.1/.github/workflows/000077500000000000000000000000001476016373400206535ustar00rootroot00000000000000json-log-formatter-1.1.1/.github/workflows/ci.yml000066400000000000000000000014501476016373400217710ustar00rootroot00000000000000on: push: branches: - master pull_request: branches: - master jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest ujson simplejson django - name: Lint with flake8 run: | flake8 . --count --show-source --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest -s tests.py json-log-formatter-1.1.1/.gitignore000066400000000000000000000001271476016373400172460ustar00rootroot00000000000000*.pyc .DS_Store __pycache__ /MANIFEST /dist /.tox /.cache /JSON_log_formatter.egg-info json-log-formatter-1.1.1/LICENSE000066400000000000000000000020751476016373400162670ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2024 Marsel Mavletkulov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. json-log-formatter-1.1.1/MANIFEST.in000066400000000000000000000000231476016373400170070ustar00rootroot00000000000000include README.rst json-log-formatter-1.1.1/Makefile000066400000000000000000000001101476016373400167060ustar00rootroot00000000000000test: tox pypi: python setup.py sdist python -m twine upload dist/* json-log-formatter-1.1.1/README.rst000066400000000000000000000117201476016373400167460ustar00rootroot00000000000000===================== JSON log formatter 🪵 ===================== The library helps you to store logs in JSON format. Why is it important? Well, it facilitates integration with **Logstash**. Usage example: .. code-block:: python import logging import json_log_formatter formatter = json_log_formatter.JSONFormatter() json_handler = logging.FileHandler(filename='/var/log/my-log.json') json_handler.setFormatter(formatter) logger = logging.getLogger('my_json') logger.addHandler(json_handler) logger.setLevel(logging.INFO) logger.info('Sign up', extra={'referral_code': '52d6ce'}) try: raise ValueError('something wrong') except ValueError: logger.error('Request failed', exc_info=True) The log file will contain the following log record (inline). .. code-block:: json { "message": "Sign up", "time": "2015-09-01T06:06:26.524448", "referral_code": "52d6ce" } { "message": "Request failed", "time": "2015-09-01T06:06:26.524449", "exc_info": "Traceback (most recent call last): ..." } If you use a log collection and analysis system, you might need to include the built-in `log record attributes `_ with ``VerboseJSONFormatter``. .. code-block:: python json_handler.setFormatter(json_log_formatter.VerboseJSONFormatter()) logger.error('An error has occured') .. code-block:: json { "filename": "tests.py", "funcName": "test_file_name_is_testspy", "levelname": "ERROR", "lineno": 276, "module": "tests", "name": "my_json", "pathname": "/Users/bob/json-log-formatter/tests.py", "process": 3081, "processName": "MainProcess", "stack_info": null, "thread": 4664270272, "threadName": "MainThread", "message": "An error has occured", "time": "2021-07-04T21:05:42.767726" } If you need to flatten complex objects as strings, use ``FlatJSONFormatter``. .. code-block:: python json_handler.setFormatter(json_log_formatter.FlatJSONFormatter()) logger.error('An error has occured') logger.info('Sign up', extra={'request': WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), })}) .. code-block:: json { "message": "Sign up", "time": "2024-10-01T00:59:29.332888+00:00", "request": "" } JSON libraries -------------- You can use **ujson** or **simplejson** instead of built-in **json** library. .. code-block:: python import json_log_formatter import ujson formatter = json_log_formatter.JSONFormatter() formatter.json_lib = ujson Note, **ujson** doesn't support ``dumps(default=f)`` argument: if it can't serialize an attribute, it might fail with ``TypeError`` or skip an attribute. Django integration ------------------ Here is an example of how the JSON formatter can be used with Django. .. code-block:: python LOGGING['formatters']['json'] = { '()': 'json_log_formatter.JSONFormatter', } LOGGING['handlers']['json_file'] = { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': '/var/log/my-log.json', 'formatter': 'json', } LOGGING['loggers']['my_json'] = { 'handlers': ['json_file'], 'level': 'INFO', } Let's try to log something. .. code-block:: python import logging logger = logging.getLogger('my_json') logger.info('Sign up', extra={'referral_code': '52d6ce'}) Custom formatter ---------------- You will likely need a custom log formatter. For instance, you want to log a user ID, an IP address and ``time`` as ``django.utils.timezone.now()``. To do so you should override ``JSONFormatter.json_record()``. .. code-block:: python class CustomisedJSONFormatter(json_log_formatter.JSONFormatter): def json_record(self, message: str, extra: dict, record: logging.LogRecord) -> dict: extra['message'] = message extra['user_id'] = current_user_id() extra['ip'] = current_ip() # Include builtins extra['level'] = record.levelname extra['name'] = record.name if 'time' not in extra: extra['time'] = django.utils.timezone.now() if record.exc_info: extra['exc_info'] = self.formatException(record.exc_info) return extra Let's say you want ``datetime`` to be serialized as timestamp. You can use **ujson** (which does it by default) and disable ISO8601 date mutation. .. code-block:: python class CustomisedJSONFormatter(json_log_formatter.JSONFormatter): json_lib = ujson def mutate_json_record(self, json_record): return json_record Tests ----- .. code-block:: console $ pip install -r requirements.txt $ tox json-log-formatter-1.1.1/json_log_formatter/000077500000000000000000000000001476016373400211535ustar00rootroot00000000000000json-log-formatter-1.1.1/json_log_formatter/__init__.py000066400000000000000000000163531476016373400232740ustar00rootroot00000000000000import logging from decimal import Decimal from datetime import datetime, timezone import json BUILTIN_ATTRS = { 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', 'funcName', 'levelname', 'levelno', 'lineno', 'module', 'msecs', 'message', 'msg', 'name', 'pathname', 'process', 'processName', 'relativeCreated', 'stack_info', 'taskName', 'thread', 'threadName', } class JSONFormatter(logging.Formatter): """JSON log formatter. Usage example:: import logging import json_log_formatter json_handler = logging.FileHandler(filename='/var/log/my-log.json') json_handler.setFormatter(json_log_formatter.JSONFormatter()) logger = logging.getLogger('my_json') logger.addHandler(json_handler) logger.info('Sign up', extra={'referral_code': '52d6ce'}) The log file will contain the following log record (inline):: { "message": "Sign up", "time": "2015-09-01T06:06:26.524448", "referral_code": "52d6ce" } """ json_lib = json def format(self, record): message = record.getMessage() extra = self.extra_from_record(record) json_record = self.json_record(message, extra, record) mutated_record = self.mutate_json_record(json_record) # Backwards compatibility: Functions that overwrite this but don't # return a new value will return None because they modified the # argument passed in. if mutated_record is None: mutated_record = json_record return self.to_json(mutated_record) def to_json(self, record): """Converts record dict to a JSON string. It makes best effort to serialize a record (represents an object as a string) instead of raising TypeError if json library supports default argument. Note, ujson doesn't support it. ValueError and OverflowError are also caught to avoid crashing an app, e.g., due to circular reference. Override this method to change the way dict is converted to JSON. """ try: return self.json_lib.dumps(record, default=_json_serializable) # ujson doesn't support default argument and raises TypeError. # "ValueError: Circular reference detected" is raised # when there is a reference to object inside the object itself. except (TypeError, ValueError, OverflowError): try: return self.json_lib.dumps(record) except (TypeError, ValueError, OverflowError): return '{}' def extra_from_record(self, record): """Returns `extra` dict you passed to logger. The `extra` keyword argument is used to populate the `__dict__` of the `LogRecord`. """ return { attr_name: record.__dict__[attr_name] for attr_name in record.__dict__ if attr_name not in BUILTIN_ATTRS } def json_record(self, message, extra, record): """Prepares a JSON payload which will be logged. Override this method to change JSON log format. :param message: Log message, e.g., `logger.info(msg='Sign up')`. :param extra: Dictionary that was passed as `extra` param `logger.info('Sign up', extra={'referral_code': '52d6ce'})`. :param record: `LogRecord` we got from `JSONFormatter.format()`. :return: Dictionary which will be passed to JSON lib. """ extra['message'] = message if 'time' not in extra: extra['time'] = datetime.now(timezone.utc) if record.exc_info: extra['exc_info'] = self.formatException(record.exc_info) return extra def mutate_json_record(self, json_record): """Override it to convert fields of `json_record` to needed types. Default implementation converts `datetime` to string in ISO8601 format. """ for attr_name in json_record: attr = json_record[attr_name] if isinstance(attr, datetime): json_record[attr_name] = attr.isoformat() return json_record def _json_serializable(obj): try: return obj.__dict__ except AttributeError: return str(obj) class VerboseJSONFormatter(JSONFormatter): """JSON log formatter with built-in log record attributes such as log level. Usage example:: import logging import json_log_formatter json_handler = logging.FileHandler(filename='/var/log/my-log.json') json_handler.setFormatter(json_log_formatter.VerboseJSONFormatter()) logger = logging.getLogger('my_verbose_json') logger.addHandler(json_handler) logger.error('An error has occured') The log file will contain the following log record (inline):: { "filename": "tests.py", "funcName": "test_file_name_is_testspy", "levelname": "ERROR", "lineno": 276, "module": "tests", "name": "my_verbose_json", "pathname": "/Users/bob/json-log-formatter/tests.py", "process": 3081, "processName": "MainProcess", "stack_info": null, "thread": 4664270272, "threadName": "MainThread", "message": "An error has occured", "time": "2021-07-04T21:05:42.767726" } Read more about the built-in log record attributes https://docs.python.org/3/library/logging.html#logrecord-attributes. """ def json_record(self, message, extra, record): extra['filename'] = record.filename extra['funcName'] = record.funcName extra['levelname'] = record.levelname extra['lineno'] = record.lineno extra['module'] = record.module extra['name'] = record.name extra['pathname'] = record.pathname extra['process'] = record.process extra['processName'] = record.processName if hasattr(record, 'stack_info'): extra['stack_info'] = record.stack_info else: extra['stack_info'] = None extra['thread'] = record.thread extra['threadName'] = record.threadName return super(VerboseJSONFormatter, self).json_record(message, extra, record) class FlatJSONFormatter(JSONFormatter): """Flat JSON log formatter ensures that complex objects are stored as strings. Usage example:: logger.info('Sign up', extra={'request': WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), })}) The log file will contain the following log record (inline):: { "message": "Sign up", "time": "2024-10-01T00:59:29.332888+00:00", "request": "" } """ keep = (bool, int, float, Decimal, complex, str, datetime) def json_record(self, message, extra, record): extra = super(FlatJSONFormatter, self).json_record(message, extra, record) return { k: v if v is None or isinstance(v, self.keep) else str(v) for k, v in extra.items() } json-log-formatter-1.1.1/pyproject.toml000066400000000000000000000013201476016373400201660ustar00rootroot00000000000000[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["json_log_formatter"] [project] name = "JSON-log-formatter" version = "1.1.1" description = "JSON log formatter" readme = "README.rst" requires-python = ">=3.6" license = {text = "MIT"} authors = [ {name = "Marsel Mavletkulov"}, ] classifiers=[ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules" ] [project.urls] repository = "https://github.com/marselester/json-log-formatter" json-log-formatter-1.1.1/requirements.txt000066400000000000000000000000221476016373400205340ustar00rootroot00000000000000-e . tox==4.11.4 json-log-formatter-1.1.1/setup.py000066400000000000000000000012131476016373400167650ustar00rootroot00000000000000from setuptools import setup setup( name='JSON-log-formatter', version='1.1.1', license='MIT', packages=['json_log_formatter'], author='Marsel Mavletkulov', url='https://github.com/marselester/json-log-formatter', description='JSON log formatter', long_description=open('README.rst').read(), classifiers=[ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries :: Python Modules' ], ) json-log-formatter-1.1.1/tests.py000066400000000000000000000362411476016373400170000ustar00rootroot00000000000000from datetime import datetime from decimal import Decimal from io import BytesIO import unittest import logging import json import os.path from django.core.handlers.wsgi import WSGIRequest from django.conf import settings import ujson import simplejson try: from cStringIO import StringIO except ImportError: from io import StringIO from json_log_formatter import JSONFormatter, VerboseJSONFormatter, FlatJSONFormatter log_buffer = StringIO() json_handler = logging.StreamHandler(log_buffer) logger = logging.getLogger('test') logger.addHandler(json_handler) logger.setLevel(logging.DEBUG) logging.propagate = False DATETIME = datetime(2015, 9, 1, 6, 9, 42, 797203) DATETIME_ISO = u'2015-09-01T06:09:42.797203' settings.configure(DEBUG=True) class TestCase(unittest.TestCase): def tearDown(self): log_buffer.seek(0) log_buffer.truncate() class JSONFormatterTest(TestCase): def setUp(self): json_handler.setFormatter(JSONFormatter()) def test_given_time_is_used_in_log_record(self): logger.info('Sign up', extra={'time': DATETIME}) expected_time = '"time": "2015-09-01T06:09:42.797203"' self.assertIn(expected_time, log_buffer.getvalue()) def test_current_time_is_used_by_default_in_log_record(self): logger.info('Sign up', extra={'fizz': 'bazz'}) self.assertNotIn(DATETIME_ISO, log_buffer.getvalue()) def test_message_and_time_are_in_json_record_when_extra_is_blank(self): logger.info('Sign up') json_record = json.loads(log_buffer.getvalue()) expected_fields = set([ 'message', 'time', ]) self.assertTrue(expected_fields.issubset(json_record)) def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided(self): logger.info('Sign up', extra={'fizz': 'bazz'}) json_record = json.loads(log_buffer.getvalue()) expected_fields = set([ 'message', 'time', 'fizz', ]) self.assertTrue(expected_fields.issubset(json_record)) def test_exc_info_is_logged(self): try: raise ValueError('something wrong') except ValueError: logger.error('Request failed', exc_info=True) json_record = json.loads(log_buffer.getvalue()) self.assertIn( 'Traceback (most recent call last)', json_record['exc_info'] ) class MutatingFormatter(JSONFormatter): def mutate_json_record(self, json_record): new_record = {} for k, v in json_record.items(): if isinstance(v, datetime): v = v.isoformat() new_record[k] = v return new_record class MutatingFormatterTest(TestCase): def setUp(self): json_handler.setFormatter(MutatingFormatter()) def test_new_record_accepted(self): logger.info('Sign up', extra={'fizz': DATETIME}) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['fizz'], DATETIME_ISO) class JsonLibTest(TestCase): def setUp(self): json_handler.setFormatter(JSONFormatter()) def test_builtin_types_are_serialized(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'first_name': 'bob', 'amount': 0.00497265, 'context': { 'tags': ['fizz', 'bazz'], }, 'things': ('a', 'b'), 'ok': True, 'none': None, }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['first_name'], 'bob') self.assertEqual(json_record['amount'], 0.00497265) self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) self.assertEqual(json_record['things'], ['a', 'b']) self.assertEqual(json_record['ok'], True) self.assertEqual(json_record['none'], None) def test_decimal_is_serialized_as_string(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'amount': Decimal('0.00497265') }) expected_amount = '"amount": "0.00497265"' self.assertIn(expected_amount, log_buffer.getvalue()) def test_django_wsgi_request_is_serialized_as_dict(self): request = WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), }) logger.log(level=logging.ERROR, msg='Django response error', extra={ 'status_code': 500, 'request': request }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['status_code'], 500) self.assertEqual(json_record['request']['path'], '/bogus') self.assertEqual(json_record['request']['method'], 'BOGUS') def test_json_circular_reference_is_handled(self): d = {} d['circle'] = d logger.info('Referer checking', extra=d) self.assertEqual('{}\n', log_buffer.getvalue()) class UjsonLibTest(TestCase): def setUp(self): formatter = JSONFormatter() formatter.json_lib = ujson json_handler.setFormatter(formatter) def test_builtin_types_are_serialized(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'first_name': 'bob', 'amount': 0.00497265, 'context': { 'tags': ['fizz', 'bazz'], }, 'things': ('a', 'b'), 'ok': True, 'none': None, }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['first_name'], 'bob') self.assertEqual(json_record['amount'], 0.00497265) self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) self.assertEqual(json_record['things'], ['a', 'b']) self.assertEqual(json_record['ok'], True) self.assertEqual(json_record['none'], None) def test_decimal_is_serialized_as_number(self): logger.info('Payment was sent', extra={ 'amount': Decimal('0.00497265') }) expected_amount = '"amount":0.00497265' self.assertIn(expected_amount, log_buffer.getvalue()) def test_zero_expected_when_decimal_is_in_scientific_notation(self): logger.info('Payment was sent', extra={ 'amount': Decimal('0E-8') }) expected_amount = '"amount":0.0' self.assertIn(expected_amount, log_buffer.getvalue()) def test_django_wsgi_request_is_serialized_as_empty_list(self): request = WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), }) logger.log(level=logging.ERROR, msg='Django response error', extra={ 'status_code': 500, 'request': request }) json_record = json.loads(log_buffer.getvalue()) if 'status_code' in json_record: self.assertEqual(json_record['status_code'], 500) if 'request' in json_record: self.assertEqual(json_record['request']['path'], '/bogus') self.assertEqual(json_record['request']['method'], 'BOGUS') def test_json_circular_reference_is_handled(self): d = {} d['circle'] = d logger.info('Referer checking', extra=d) self.assertEqual('{}\n', log_buffer.getvalue()) class SimplejsonLibTest(TestCase): def setUp(self): formatter = JSONFormatter() formatter.json_lib = simplejson json_handler.setFormatter(formatter) def test_builtin_types_are_serialized(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'first_name': 'bob', 'amount': 0.00497265, 'context': { 'tags': ['fizz', 'bazz'], }, 'things': ('a', 'b'), 'ok': True, 'none': None, }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['first_name'], 'bob') self.assertEqual(json_record['amount'], 0.00497265) self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) self.assertEqual(json_record['things'], ['a', 'b']) self.assertEqual(json_record['ok'], True) self.assertEqual(json_record['none'], None) def test_decimal_is_serialized_as_number(self): logger.info('Payment was sent', extra={ 'amount': Decimal('0.00497265') }) expected_amount = '"amount": 0.00497265' self.assertIn(expected_amount, log_buffer.getvalue()) def test_decimal_is_serialized_as_it_is_when_it_is_in_scientific_notation(self): logger.info('Payment was sent', extra={ 'amount': Decimal('0E-8') }) expected_amount = '"amount": 0E-8' self.assertIn(expected_amount, log_buffer.getvalue()) def test_django_wsgi_request_is_serialized_as_dict(self): request = WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), }) logger.log(level=logging.ERROR, msg='Django response error', extra={ 'status_code': 500, 'request': request }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['status_code'], 500) self.assertEqual(json_record['request']['path'], '/bogus') self.assertEqual(json_record['request']['method'], 'BOGUS') def test_json_circular_reference_is_handled(self): d = {} d['circle'] = d logger.info('Referer checking', extra=d) self.assertEqual('{}\n', log_buffer.getvalue()) class VerboseJSONFormatterTest(TestCase): def setUp(self): json_handler.setFormatter(VerboseJSONFormatter()) def test_file_name_is_testspy(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['filename'], 'tests.py') def test_function_name(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['funcName'], 'test_function_name') def test_level_name_is_error(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['levelname'], 'ERROR') def test_module_name_is_tests(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['module'], 'tests') def test_logger_name_is_test(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['name'], 'test') def test_path_name_is_test(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertIn(os.path.basename(os.path.abspath('.')) + '/tests.py', json_record['pathname']) def test_process_name_is_MainProcess(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['processName'], 'MainProcess') def test_thread_name_is_MainThread(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['threadName'], 'MainThread') def test_stack_info_is_none(self): logger.error('An error has occured') json_record = json.loads(log_buffer.getvalue()) self.assertIsNone(json_record['stack_info']) class FlatJSONFormatterTest(TestCase): def setUp(self): json_handler.setFormatter(FlatJSONFormatter()) def test_given_time_is_used_in_log_record(self): logger.info('Sign up', extra={'time': DATETIME}) expected_time = '"time": "2015-09-01T06:09:42.797203"' self.assertIn(expected_time, log_buffer.getvalue()) def test_current_time_is_used_by_default_in_log_record(self): logger.info('Sign up', extra={'fizz': 'bazz'}) self.assertNotIn(DATETIME_ISO, log_buffer.getvalue()) def test_message_and_time_are_in_json_record_when_extra_is_blank(self): logger.info('Sign up') json_record = json.loads(log_buffer.getvalue()) expected_fields = set([ 'message', 'time', ]) self.assertTrue(expected_fields.issubset(json_record)) def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided(self): logger.info('Sign up', extra={'fizz': 'bazz'}) json_record = json.loads(log_buffer.getvalue()) expected_fields = set([ 'message', 'time', 'fizz', ]) self.assertTrue(expected_fields.issubset(json_record)) def test_exc_info_is_logged(self): try: raise ValueError('something wrong') except ValueError: logger.error('Request failed', exc_info=True) json_record = json.loads(log_buffer.getvalue()) self.assertIn( 'Traceback (most recent call last)', json_record['exc_info'] ) def test_builtin_types_are_serialized(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'first_name': 'bob', 'amount': 0.00497265, 'context': { 'tags': ['fizz', 'bazz'], }, 'things': ('a', 'b'), 'ok': True, 'none': None, }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['first_name'], 'bob') self.assertEqual(json_record['amount'], 0.00497265) self.assertEqual(json_record['context'], "{'tags': ['fizz', 'bazz']}") self.assertEqual(json_record['things'], "('a', 'b')") self.assertEqual(json_record['ok'], True) self.assertEqual(json_record['none'], None) def test_decimal_is_serialized_as_string(self): logger.log(level=logging.ERROR, msg='Payment was sent', extra={ 'amount': Decimal('0.00497265') }) expected_amount = '"amount": "0.00497265"' self.assertIn(expected_amount, log_buffer.getvalue()) def test_django_wsgi_request_is_serialized_as_dict(self): request = WSGIRequest({ 'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'CONTENT_TYPE': 'text/html; charset=utf8', 'wsgi.input': BytesIO(b''), }) logger.log(level=logging.ERROR, msg='Django response error', extra={ 'status_code': 500, 'request': request, 'dict': { 'request': request, }, 'list': [request], }) json_record = json.loads(log_buffer.getvalue()) self.assertEqual(json_record['status_code'], 500) self.assertEqual(json_record['request'], "") self.assertEqual(json_record['dict'], "{'request': }") self.assertEqual(json_record['list'], "[]") json-log-formatter-1.1.1/tox.ini000066400000000000000000000002251476016373400165700ustar00rootroot00000000000000[tox] envlist=py36,py37,py38,py39,py310,py311,py312 [testenv] deps= pytest ujson simplejson django commands= pytest -s tests.py