Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f57888e

Browse files
Merge pull request #36 from marselester/flat-formatter
FlatJSONFormatter
2 parents 1b20bcb + eeac2e9 commit f57888e

File tree

3 files changed

+149
-1
lines changed

3 files changed

+149
-1
lines changed

‎README.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ with ``VerboseJSONFormatter``.
7373
"time": "2021年07月04日T21:05:42.767726"
7474
}
7575
76+
If you need to flatten complex objects as strings, use ``FlatJSONFormatter``.
77+
78+
.. code-block:: python
79+
80+
json_handler.setFormatter(json_log_formatter.FlatJSONFormatter())
81+
logger.error('An error has occured')
82+
83+
logger.info('Sign up', extra={'request': WSGIRequest({
84+
'PATH_INFO': 'bogus',
85+
'REQUEST_METHOD': 'bogus',
86+
'CONTENT_TYPE': 'text/html; charset=utf8',
87+
'wsgi.input': BytesIO(b''),
88+
})})
89+
90+
.. code-block:: json
91+
92+
{
93+
"message": "Sign up",
94+
"time": "2024年10月01日T00:59:29.332888+00:00",
95+
"request": "<WSGIRequest: BOGUS '/bogus'>"
96+
}
97+
7698
JSON libraries
7799
--------------
78100

‎json_log_formatter/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from decimal import Decimal
23
from datetime import datetime, timezone
34

45
import json
@@ -204,3 +205,35 @@ def json_record(self, message, extra, record):
204205
extra['thread'] = record.thread
205206
extra['threadName'] = record.threadName
206207
return super(VerboseJSONFormatter, self).json_record(message, extra, record)
208+
209+
210+
class FlatJSONFormatter(JSONFormatter):
211+
"""Flat JSON log formatter ensures that complex objects are stored as strings.
212+
213+
Usage example::
214+
215+
logger.info('Sign up', extra={'request': WSGIRequest({
216+
'PATH_INFO': 'bogus',
217+
'REQUEST_METHOD': 'bogus',
218+
'CONTENT_TYPE': 'text/html; charset=utf8',
219+
'wsgi.input': BytesIO(b''),
220+
})})
221+
222+
The log file will contain the following log record (inline)::
223+
224+
{
225+
"message": "Sign up",
226+
"time": "2024年10月01日T00:59:29.332888+00:00",
227+
"request": "<WSGIRequest: BOGUS '/bogus'>"
228+
}
229+
230+
"""
231+
232+
keep = (bool, int, float, Decimal, complex, str, datetime)
233+
234+
def json_record(self, message, extra, record):
235+
extra = super(FlatJSONFormatter, self).json_record(message, extra, record)
236+
return {
237+
k: v if v is None or isinstance(v, self.keep) else str(v)
238+
for k, v in extra.items()
239+
}

‎tests.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
except ImportError:
1818
from io import StringIO
1919

20-
from json_log_formatter import JSONFormatter, VerboseJSONFormatter
20+
from json_log_formatter import JSONFormatter, VerboseJSONFormatter, FlatJSONFormatter
2121

2222
log_buffer = StringIO()
2323
json_handler = logging.StreamHandler(log_buffer)
@@ -336,3 +336,96 @@ def test_stack_info_is_none(self):
336336
logger.error('An error has occured')
337337
json_record = json.loads(log_buffer.getvalue())
338338
self.assertIsNone(json_record['stack_info'])
339+
340+
341+
class FlatJSONFormatterTest(TestCase):
342+
def setUp(self):
343+
json_handler.setFormatter(FlatJSONFormatter())
344+
345+
def test_given_time_is_used_in_log_record(self):
346+
logger.info('Sign up', extra={'time': DATETIME})
347+
expected_time = '"time": "2015年09月01日T06:09:42.797203"'
348+
self.assertIn(expected_time, log_buffer.getvalue())
349+
350+
def test_current_time_is_used_by_default_in_log_record(self):
351+
logger.info('Sign up', extra={'fizz': 'bazz'})
352+
self.assertNotIn(DATETIME_ISO, log_buffer.getvalue())
353+
354+
def test_message_and_time_are_in_json_record_when_extra_is_blank(self):
355+
logger.info('Sign up')
356+
json_record = json.loads(log_buffer.getvalue())
357+
expected_fields = set([
358+
'message',
359+
'time',
360+
])
361+
self.assertTrue(expected_fields.issubset(json_record))
362+
363+
def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided(self):
364+
logger.info('Sign up', extra={'fizz': 'bazz'})
365+
json_record = json.loads(log_buffer.getvalue())
366+
expected_fields = set([
367+
'message',
368+
'time',
369+
'fizz',
370+
])
371+
self.assertTrue(expected_fields.issubset(json_record))
372+
373+
def test_exc_info_is_logged(self):
374+
try:
375+
raise ValueError('something wrong')
376+
except ValueError:
377+
logger.error('Request failed', exc_info=True)
378+
json_record = json.loads(log_buffer.getvalue())
379+
self.assertIn(
380+
'Traceback (most recent call last)',
381+
json_record['exc_info']
382+
)
383+
384+
def test_builtin_types_are_serialized(self):
385+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
386+
'first_name': 'bob',
387+
'amount': 0.00497265,
388+
'context': {
389+
'tags': ['fizz', 'bazz'],
390+
},
391+
'things': ('a', 'b'),
392+
'ok': True,
393+
'none': None,
394+
})
395+
396+
json_record = json.loads(log_buffer.getvalue())
397+
self.assertEqual(json_record['first_name'], 'bob')
398+
self.assertEqual(json_record['amount'], 0.00497265)
399+
self.assertEqual(json_record['context'], "{'tags': ['fizz', 'bazz']}")
400+
self.assertEqual(json_record['things'], "('a', 'b')")
401+
self.assertEqual(json_record['ok'], True)
402+
self.assertEqual(json_record['none'], None)
403+
404+
def test_decimal_is_serialized_as_string(self):
405+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
406+
'amount': Decimal('0.00497265')
407+
})
408+
expected_amount = '"amount": "0.00497265"'
409+
self.assertIn(expected_amount, log_buffer.getvalue())
410+
411+
def test_django_wsgi_request_is_serialized_as_dict(self):
412+
request = WSGIRequest({
413+
'PATH_INFO': 'bogus',
414+
'REQUEST_METHOD': 'bogus',
415+
'CONTENT_TYPE': 'text/html; charset=utf8',
416+
'wsgi.input': BytesIO(b''),
417+
})
418+
419+
logger.log(level=logging.ERROR, msg='Django response error', extra={
420+
'status_code': 500,
421+
'request': request,
422+
'dict': {
423+
'request': request,
424+
},
425+
'list': [request],
426+
})
427+
json_record = json.loads(log_buffer.getvalue())
428+
self.assertEqual(json_record['status_code'], 500)
429+
self.assertEqual(json_record['request'], "<WSGIRequest: BOGUS '/bogus'>")
430+
self.assertEqual(json_record['dict'], "{'request': <WSGIRequest: BOGUS '/bogus'>}")
431+
self.assertEqual(json_record['list'], "[<WSGIRequest: BOGUS '/bogus'>]")

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /