Skip to content

Commit f078b11

Browse files
authored
Merge pull request #13 from marselester/json-best-effort
Make best effort to serialize json, e.g., WSGIRequest
2 parents 51b8046 + 7535a68 commit f078b11

File tree

5 files changed

+147
-12
lines changed

5 files changed

+147
-12
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ python:
77
- 3.7
88
- 3.8
99
install:
10-
- pip install ujson simplejson
10+
- pip install ujson simplejson django
1111
- python setup.py install
1212
script: nosetests tests
1313
notifications:

README.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ JSON libraries
5151
--------------
5252

5353
You can use **ujson** or **simplejson** instead of built-in **json** library.
54-
They are faster and can serialize ``Decimal`` values.
5554

5655
.. code-block:: python
5756
@@ -61,6 +60,9 @@ They are faster and can serialize ``Decimal`` values.
6160
formatter = json_log_formatter.JSONFormatter()
6261
formatter.json_lib = ujson
6362
63+
Note, **ujson** doesn't support `dumps(default=f)` argument:
64+
if it can't serialize an attribute, it might fail with `TypeError` or skip an attribute.
65+
6466
Django integration
6567
------------------
6668

@@ -111,7 +113,7 @@ To do so you should override ``JSONFormatter.json_record()``.
111113
return extra
112114
113115
Let's say you want ``datetime`` to be serialized as timestamp.
114-
Then you should use **ujson** (which does it by default) and disable
116+
You can use **ujson** (which does it by default) and disable
115117
ISO8601 date mutation.
116118

117119
.. code-block:: python

json_log_formatter/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,18 @@ def format(self, record):
7373
def to_json(self, record):
7474
"""Converts record dict to a JSON string.
7575
76+
It makes best effort to serialize a record (represents an object as a string)
77+
instead of raising TypeError if json library supports default argument.
78+
Note, ujson doesn't support it.
79+
7680
Override this method to change the way dict is converted to JSON.
7781
7882
"""
79-
return self.json_lib.dumps(record)
83+
try:
84+
return self.json_lib.dumps(record, default=_json_serializable)
85+
# ujson doesn't support default argument and raises TypeError.
86+
except TypeError:
87+
return self.json_lib.dumps(record)
8088

8189
def extra_from_record(self, record):
8290
"""Returns `extra` dict you passed to logger.
@@ -123,3 +131,10 @@ def mutate_json_record(self, json_record):
123131
if isinstance(attr, datetime):
124132
json_record[attr_name] = attr.isoformat()
125133
return json_record
134+
135+
136+
def _json_serializable(obj):
137+
try:
138+
return obj.__dict__
139+
except AttributeError:
140+
return str(obj)

tests.py

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import unittest
2-
import logging
31
from datetime import datetime
42
from decimal import Decimal
5-
3+
from io import BytesIO
4+
import unittest
5+
import logging
66
import json
7+
8+
9+
from django.core.handlers.wsgi import WSGIRequest
10+
from django.conf import settings
711
import ujson
812
import simplejson
913

@@ -25,6 +29,8 @@
2529
DATETIME = datetime(2015, 9, 1, 6, 9, 42, 797203)
2630
DATETIME_ISO = u'2015-09-01T06:09:42.797203'
2731

32+
settings.configure(DEBUG=True)
33+
2834

2935
class TestCase(unittest.TestCase):
3036
def tearDown(self):
@@ -100,11 +106,49 @@ class JsonLibTest(TestCase):
100106
def setUp(self):
101107
json_handler.setFormatter(JSONFormatter())
102108

103-
def test_error_when_decimal_is_passed(self):
104-
with self.assertRaises(TypeError):
105-
logger.log(lvl=0, msg='Payment was sent', extra={
106-
'amount': Decimal('0.00497265')
107-
})
109+
def test_builtin_types_are_serialized(self):
110+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
111+
'first_name': 'bob',
112+
'amount': 0.00497265,
113+
'context': {
114+
'tags': ['fizz', 'bazz'],
115+
},
116+
'things': ('a', 'b'),
117+
'ok': True,
118+
'none': None,
119+
})
120+
121+
json_record = json.loads(log_buffer.getvalue())
122+
self.assertEqual(json_record['first_name'], 'bob')
123+
self.assertEqual(json_record['amount'], 0.00497265)
124+
self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']})
125+
self.assertEqual(json_record['things'], ['a', 'b'])
126+
self.assertEqual(json_record['ok'], True)
127+
self.assertEqual(json_record['none'], None)
128+
129+
def test_decimal_is_serialized_as_string(self):
130+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
131+
'amount': Decimal('0.00497265')
132+
})
133+
expected_amount = '"amount": "0.00497265"'
134+
self.assertIn(expected_amount, log_buffer.getvalue())
135+
136+
def test_django_wsgi_request_is_serialized_as_dict(self):
137+
request = WSGIRequest({
138+
'PATH_INFO': 'bogus',
139+
'REQUEST_METHOD': 'bogus',
140+
'CONTENT_TYPE': 'text/html; charset=utf8',
141+
'wsgi.input': BytesIO(b''),
142+
})
143+
144+
logger.log(level=logging.ERROR, msg='Django response error', extra={
145+
'status_code': 500,
146+
'request': request
147+
})
148+
json_record = json.loads(log_buffer.getvalue())
149+
self.assertEqual(json_record['status_code'], 500)
150+
self.assertEqual(json_record['request']['path'], '/bogus')
151+
self.assertEqual(json_record['request']['method'], 'BOGUS')
108152

109153

110154
class UjsonLibTest(TestCase):
@@ -113,6 +157,26 @@ def setUp(self):
113157
formatter.json_lib = ujson
114158
json_handler.setFormatter(formatter)
115159

160+
def test_builtin_types_are_serialized(self):
161+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
162+
'first_name': 'bob',
163+
'amount': 0.00497265,
164+
'context': {
165+
'tags': ['fizz', 'bazz'],
166+
},
167+
'things': ('a', 'b'),
168+
'ok': True,
169+
'none': None,
170+
})
171+
172+
json_record = json.loads(log_buffer.getvalue())
173+
self.assertEqual(json_record['first_name'], 'bob')
174+
self.assertEqual(json_record['amount'], 0.00497265)
175+
self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']})
176+
self.assertEqual(json_record['things'], ['a', 'b'])
177+
self.assertEqual(json_record['ok'], True)
178+
self.assertEqual(json_record['none'], None)
179+
116180
def test_decimal_is_serialized_as_number(self):
117181
logger.info('Payment was sent', extra={
118182
'amount': Decimal('0.00497265')
@@ -127,13 +191,49 @@ def test_zero_expected_when_decimal_is_in_scientific_notation(self):
127191
expected_amount = '"amount":0.0'
128192
self.assertIn(expected_amount, log_buffer.getvalue())
129193

194+
def test_django_wsgi_request_is_serialized_as_empty_list(self):
195+
request = WSGIRequest({
196+
'PATH_INFO': 'bogus',
197+
'REQUEST_METHOD': 'bogus',
198+
'CONTENT_TYPE': 'text/html; charset=utf8',
199+
'wsgi.input': BytesIO(b''),
200+
})
201+
202+
logger.log(level=logging.ERROR, msg='Django response error', extra={
203+
'status_code': 500,
204+
'request': request
205+
})
206+
json_record = json.loads(log_buffer.getvalue())
207+
self.assertEqual(json_record['status_code'], 500)
208+
self.assertEqual(json_record['request'], [])
209+
130210

131211
class SimplejsonLibTest(TestCase):
132212
def setUp(self):
133213
formatter = JSONFormatter()
134214
formatter.json_lib = simplejson
135215
json_handler.setFormatter(formatter)
136216

217+
def test_builtin_types_are_serialized(self):
218+
logger.log(level=logging.ERROR, msg='Payment was sent', extra={
219+
'first_name': 'bob',
220+
'amount': 0.00497265,
221+
'context': {
222+
'tags': ['fizz', 'bazz'],
223+
},
224+
'things': ('a', 'b'),
225+
'ok': True,
226+
'none': None,
227+
})
228+
229+
json_record = json.loads(log_buffer.getvalue())
230+
self.assertEqual(json_record['first_name'], 'bob')
231+
self.assertEqual(json_record['amount'], 0.00497265)
232+
self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']})
233+
self.assertEqual(json_record['things'], ['a', 'b'])
234+
self.assertEqual(json_record['ok'], True)
235+
self.assertEqual(json_record['none'], None)
236+
137237
def test_decimal_is_serialized_as_number(self):
138238
logger.info('Payment was sent', extra={
139239
'amount': Decimal('0.00497265')
@@ -147,3 +247,20 @@ def test_decimal_is_serialized_as_it_is_when_it_is_in_scientific_notation(self):
147247
})
148248
expected_amount = '"amount": 0E-8'
149249
self.assertIn(expected_amount, log_buffer.getvalue())
250+
251+
def test_django_wsgi_request_is_serialized_as_dict(self):
252+
request = WSGIRequest({
253+
'PATH_INFO': 'bogus',
254+
'REQUEST_METHOD': 'bogus',
255+
'CONTENT_TYPE': 'text/html; charset=utf8',
256+
'wsgi.input': BytesIO(b''),
257+
})
258+
259+
logger.log(level=logging.ERROR, msg='Django response error', extra={
260+
'status_code': 500,
261+
'request': request
262+
})
263+
json_record = json.loads(log_buffer.getvalue())
264+
self.assertEqual(json_record['status_code'], 500)
265+
self.assertEqual(json_record['request']['path'], '/bogus')
266+
self.assertEqual(json_record['request']['method'], 'BOGUS')

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ deps=
66
pytest
77
ujson
88
simplejson
9+
django
910
commands=
1011
pytest -s tests.py

0 commit comments

Comments
 (0)