Skip to content

Commit bba16b0

Browse files
committed
Add FlatJSONFormatter
1 parent 1b20bcb commit bba16b0

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

json_log_formatter/__init__.py

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

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-01T06: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)