Skip to content

Commit 8b733ec

Browse files
committed
Add ability to override default serialization (elastic#2018)
* utils: add simplejson based JSON handling Add an alternative json serialization and deserialization implementation using simplejson configured to be JSON compliant by converting floats like nan, +inf and -inf to null . The idea is to permit users to use a different serialization if they need a different behaviour. Refs elastic#1886 * client: fix overriding of transport json serializer We get a string from config so need to import it.
1 parent 622c78f commit 8b733ec

File tree

7 files changed

+168
-6
lines changed

7 files changed

+168
-6
lines changed

elasticapm/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ def __init__(self, config=None, **inline) -> None:
155155
"processors": self.load_processors(),
156156
}
157157
if config.transport_json_serializer:
158-
transport_kwargs["json_serializer"] = config.transport_json_serializer
158+
json_serializer_func = import_string(config.transport_json_serializer)
159+
transport_kwargs["json_serializer"] = json_serializer_func
159160

160161
self._api_endpoint_url = urllib.parse.urljoin(
161162
self.config.server_url if self.config.server_url.endswith("/") else self.config.server_url + "/",

elasticapm/utils/json_encoder.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,9 @@
3131

3232
import datetime
3333
import decimal
34+
import json
3435
import uuid
3536

36-
try:
37-
import json
38-
except ImportError:
39-
import simplejson as json
40-
4137

4238
class BetterJSONEncoder(json.JSONEncoder):
4339
ENCODERS = {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
4+
# Copyright (c) 2019, Elasticsearch BV
5+
# All rights reserved.
6+
#
7+
# Redistribution and use in source and binary forms, with or without
8+
# modification, are permitted provided that the following conditions are met:
9+
#
10+
# * Redistributions of source code must retain the above copyright notice, this
11+
# list of conditions and the following disclaimer.
12+
#
13+
# * Redistributions in binary form must reproduce the above copyright notice,
14+
# this list of conditions and the following disclaimer in the documentation
15+
# and/or other materials provided with the distribution.
16+
#
17+
# * Neither the name of the copyright holder nor the names of its
18+
# contributors may be used to endorse or promote products derived from
19+
# this software without specific prior written permission.
20+
#
21+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+
31+
32+
import simplejson as json
33+
34+
from elasticapm.utils.json_encoder import BetterJSONEncoder
35+
36+
37+
class BetterSimpleJSONEncoder(json.JSONEncoder):
38+
ENCODERS = BetterJSONEncoder.ENCODERS
39+
40+
def default(self, obj):
41+
if type(obj) in self.ENCODERS:
42+
return self.ENCODERS[type(obj)](obj)
43+
try:
44+
return super(BetterSimpleJSONEncoder, self).default(obj)
45+
except TypeError:
46+
return str(obj)
47+
48+
49+
def better_decoder(data):
50+
return data
51+
52+
53+
def dumps(value, **kwargs):
54+
return json.dumps(value, cls=BetterSimpleJSONEncoder, ignore_nan=True, **kwargs)
55+
56+
57+
def loads(value, **kwargs):
58+
return json.loads(value, object_hook=better_decoder)

tests/client/client_tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
import elasticapm
4949
from elasticapm.base import Client
5050
from elasticapm.conf.constants import ERROR
51+
52+
try:
53+
from elasticapm.utils.simplejson_encoder import dumps as simplejson_dumps
54+
except ImportError:
55+
simplejson_dumps = None
5156
from tests.fixtures import DummyTransport, TempStoreClient
5257
from tests.utils import assert_any_record_contains
5358

@@ -228,6 +233,14 @@ def test_custom_transport(elasticapm_client):
228233
assert isinstance(elasticapm_client._transport, DummyTransport)
229234

230235

236+
@pytest.mark.skipIf(simplejson_dumps is None)
237+
@pytest.mark.parametrize(
238+
"elasticapm_client", [{"transport_json_serializer": "elasticapm.utils.simplejson_encoder.dumps"}], indirect=True
239+
)
240+
def test_custom_transport_json_serializer(elasticapm_client):
241+
assert elasticapm_client._transport._json_serializer == simplejson_dumps
242+
243+
231244
@pytest.mark.parametrize("elasticapm_client", [{"processors": []}], indirect=True)
232245
def test_empty_processor_list(elasticapm_client):
233246
assert elasticapm_client.processors == []

tests/requirements/reqs-base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pytz
3030
ecs_logging
3131
structlog
3232
wrapt>=1.14.1,<1.15.0
33+
simplejson
3334

3435
pytest-asyncio==0.21.0 ; python_version >= '3.7'
3536
asynctest==0.13.0 ; python_version >= '3.7'

tests/utils/json_utils/tests.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import decimal
3737
import uuid
3838

39+
import pytest
40+
3941
from elasticapm.utils import json_encoder as json
4042

4143

@@ -69,6 +71,11 @@ def test_decimal():
6971
assert json.dumps(res) == "1.0"
7072

7173

74+
@pytest.mark.parametrize("res", [float("nan"), float("+inf"), float("-inf")])
75+
def test_float_invalid_json(res):
76+
assert json.dumps(res) != "null"
77+
78+
7279
def test_unsupported():
7380
res = object()
7481
assert json.dumps(res).startswith('"<object object at')
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# BSD 3-Clause License
4+
#
5+
# Copyright (c) 2019, Elasticsearch BV
6+
# All rights reserved.
7+
#
8+
# Redistribution and use in source and binary forms, with or without
9+
# modification, are permitted provided that the following conditions are met:
10+
#
11+
# * Redistributions of source code must retain the above copyright notice, this
12+
# list of conditions and the following disclaimer.
13+
#
14+
# * Redistributions in binary form must reproduce the above copyright notice,
15+
# this list of conditions and the following disclaimer in the documentation
16+
# and/or other materials provided with the distribution.
17+
#
18+
# * Neither the name of the copyright holder nor the names of its
19+
# contributors may be used to endorse or promote products derived from
20+
# this software without specific prior written permission.
21+
#
22+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32+
33+
import datetime
34+
import decimal
35+
import uuid
36+
37+
import pytest
38+
39+
simplejson = pytest.importorskip("simplejson")
40+
41+
from elasticapm.utils import simplejson_encoder as json
42+
43+
44+
def test_uuid():
45+
res = uuid.uuid4()
46+
assert json.dumps(res) == '"%s"' % res.hex
47+
48+
49+
def test_datetime():
50+
res = datetime.datetime(day=1, month=1, year=2011, hour=1, minute=1, second=1)
51+
assert json.dumps(res) == '"2011-01-01T01:01:01.000000Z"'
52+
53+
54+
def test_set():
55+
res = set(["foo", "bar"])
56+
assert json.dumps(res) in ('["foo", "bar"]', '["bar", "foo"]')
57+
58+
59+
def test_frozenset():
60+
res = frozenset(["foo", "bar"])
61+
assert json.dumps(res) in ('["foo", "bar"]', '["bar", "foo"]')
62+
63+
64+
def test_bytes():
65+
res = bytes("foobar", encoding="ascii")
66+
assert json.dumps(res) == '"foobar"'
67+
68+
69+
def test_decimal():
70+
res = decimal.Decimal("1.0")
71+
assert json.dumps(res) == "1.0"
72+
73+
74+
@pytest.mark.parametrize("res", [float("nan"), float("+inf"), float("-inf")])
75+
def test_float_invalid_json(res):
76+
assert json.dumps(res) == "null"
77+
78+
79+
def test_float():
80+
res = 1.0
81+
assert json.dumps(res) == "1.0"
82+
83+
84+
def test_unsupported():
85+
res = object()
86+
assert json.dumps(res).startswith('"<object object at')

0 commit comments

Comments
 (0)