Skip to content

Commit a971bcd

Browse files
committed
Render infinite values to valid JSON values. Fixes graphite-project#813
- The 'json' and 'jsonp' formats output float('inf') as 1e9999, float ('-inf') as -1e9999, and float('nan') as null - The 'dygraph' format (a JSON-based format) outputs the same as Infinity, -Infinity, and null (see http://dygraphs .com/tests/gviz-infinity.html) - Minor updates to documents to make sure commands can be run without errors or problems
1 parent 6811d20 commit a971bcd

File tree

5 files changed

+118
-22
lines changed

5 files changed

+118
-22
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ We are always looking to improve our test coverage. New tests will be appreciat
3232

3333
If you see a mistake, have a feature, or a performance gain, we'd love to see it. It's _strongly_ encouraged for contributions that aren't already covered by tests to come with them. We're not trying to foist work on to you, but it makes it much easier for us to accept your contributions.
3434

35+
To set up your development environment, see the instructions in requirements.txt
3536

3637
### Documentation
3738

requirements.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# This is a PIP requirements file.
22
# To setup a dev environment:
33
#
4+
# Use Python 2.7
5+
#
46
# If you use virtualenvwrapper, you can use the misc/virtualenvwrapper hook scripts to
57
# automate most of the following commands
68
#
79
# easy_install virtualenv
8-
# virtualenv --distribute --no-site-packages --prompt "(graphite venv) " .venv
10+
# virtualenv --distribute --no-site-packages --prompt "(graphite venv) " --python=/usr/bin/python2.7 .venv
911
# source .venv/bin/activate
1012
#
1113
# brew install cairo && brew link cairo # on OSX
@@ -23,7 +25,7 @@
2325
#
2426
# mkdir -p .venv/storage/log/webapp /opt/graphite/storage/
2527
# sudo chown -R $USER: /opt/graphite
26-
# .venv/bin/django-admin.py syncdb --settings=graphite.settings --pythonpath=webapp
28+
# .venv/bin/django-admin.py migrate --run-syncdb --settings=graphite.settings --pythonpath=webapp
2729
# bin/run-graphite-devel-server.py ./
2830
# # or
2931
# # cd webapp/graphite && $GRAPHITE_ROOT/.venv/bin/gunicorn_django -b 127.0.0.1:8080
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
3+
4+
class FloatEncoder(json.JSONEncoder):
5+
def __init__(self, nan_str="null", **kwargs):
6+
super(FloatEncoder, self).__init__(**kwargs)
7+
self.nan_str = nan_str
8+
9+
def iterencode(self, o, _one_shot=False):
10+
"""Encode the given object and yield each string
11+
representation as available.
12+
13+
For example::
14+
15+
for chunk in JSONEncoder().iterencode(bigobject):
16+
mysocket.write(chunk)
17+
"""
18+
if self.check_circular:
19+
markers = {}
20+
else:
21+
markers = None
22+
if self.ensure_ascii:
23+
_encoder = json.encoder.encode_basestring_ascii
24+
else:
25+
_encoder = json.encoder.encode_basestring
26+
if self.encoding != 'utf-8':
27+
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
28+
if isinstance(o, str):
29+
o = o.decode(_encoding)
30+
return _orig_encoder(o)
31+
32+
def floatstr(o, allow_nan=self.allow_nan, _repr=json.encoder.FLOAT_REPR,
33+
_inf=json.encoder.INFINITY, _neginf=-json.encoder.INFINITY,
34+
nan_str=self.nan_str):
35+
# Check for specials. Note that this type of test is processor
36+
# and/or platform-specific, so do tests which don't depend on the
37+
# internals.
38+
39+
if o != o:
40+
text = nan_str
41+
elif o == _inf:
42+
text = '1e9999'
43+
elif o == _neginf:
44+
text = '-1e9999'
45+
else:
46+
return _repr(o)
47+
48+
if not allow_nan:
49+
raise ValueError(
50+
"Out of range float values are not JSON compliant: " +
51+
repr(o))
52+
53+
return text
54+
55+
_iterencode = json.encoder._make_iterencode(
56+
markers, self.default, _encoder, self.indent, floatstr,
57+
self.key_separator, self.item_separator, self.sort_keys,
58+
self.skipkeys, _one_shot)
59+
return _iterencode(o, 0)

webapp/graphite/render/views.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from urlparse import urlsplit, urlunsplit
2323
from cgi import parse_qs
2424
from cStringIO import StringIO
25+
2526
try:
2627
import cPickle as pickle
2728
except ImportError:
@@ -36,6 +37,7 @@
3637
from graphite.render.functions import PieFunctions
3738
from graphite.render.hashing import hashRequest, hashData
3839
from graphite.render.glyph import GraphTypes
40+
from graphite.render.float_encoder import FloatEncoder
3941

4042
from django.http import HttpResponseServerError, HttpResponseRedirect
4143
from django.template import Context, loader
@@ -172,10 +174,10 @@ def renderView(request):
172174

173175
if 'jsonp' in requestOptions:
174176
response = HttpResponse(
175-
content="%s(%s)" % (requestOptions['jsonp'], json.dumps(series_data)),
177+
content="%s(%s)" % (requestOptions['jsonp'], json.dumps(series_data, cls=FloatEncoder)),
176178
content_type='text/javascript')
177179
else:
178-
response = HttpResponse(content=json.dumps(series_data),
180+
response = HttpResponse(content=json.dumps(series_data, cls=FloatEncoder),
179181
content_type='application/json')
180182

181183
if useCache:
@@ -193,7 +195,15 @@ def renderView(request):
193195
for series in data:
194196
labels.append(series.name)
195197
for i, point in enumerate(series):
196-
datapoints[i].append(point if point is not None else 'null')
198+
if point is None:
199+
point = 'null'
200+
elif point == float('inf'):
201+
point = 'Infinity'
202+
elif point == float('-inf'):
203+
point = '-Infinity'
204+
elif math.isnan(point):
205+
point = 'null'
206+
datapoints[i].append(point)
197207
line_template = '[%%s000%s]' % ''.join([', %s'] * len(data))
198208
lines = [line_template % tuple(points) for points in datapoints]
199209
result = '{"labels" : %s, "data" : [%s]}' % (json.dumps(labels), ', '.join(lines))

webapp/tests/test_render.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import time
5+
import math
56
import logging
67
import shutil
78

@@ -90,43 +91,66 @@ def test_render_view(self):
9091
whisper.create(self.db, [(1, 60)])
9192

9293
ts = int(time.time())
93-
whisper.update(self.db, 0.1234567890123456789012, ts - 2)
94-
whisper.update(self.db, 0.4, ts - 1)
95-
whisper.update(self.db, 0.6, ts)
94+
whisper.update(self.db, 0.1234567890123456789012, ts - 5)
95+
whisper.update(self.db, 0.4, ts - 4)
96+
whisper.update(self.db, 0.6, ts - 3)
97+
whisper.update(self.db, float('inf'), ts - 2)
98+
whisper.update(self.db, float('-inf'), ts - 1)
99+
whisper.update(self.db, float('nan'), ts)
96100

97101
response = self.client.get(url, {'target': 'test', 'format': 'raw'})
98102
raw_data = ("None,None,None,None,None,None,None,None,None,None,None,"
99103
"None,None,None,None,None,None,None,None,None,None,None,"
100104
"None,None,None,None,None,None,None,None,None,None,None,"
101105
"None,None,None,None,None,None,None,None,None,None,None,"
102-
"None,None,None,None,None,None,None,None,None,None,None,"
103-
"None,None,0.12345678901234568,0.4,0.6")
106+
"None,None,None,None,None,None,None,None,None,None,"
107+
"0.12345678901234568,0.4,0.6,inf,-inf,nan")
104108
raw_response = "test,%d,%d,1|%s\n" % (ts-59, ts+1, raw_data)
105109
self.assertEqual(response.content, raw_response)
106110

107111
response = self.client.get(url, {'target': 'test', 'format': 'json'})
112+
self.assertIn('[1e9999, ' + str(ts - 2) + ']', response.content)
113+
self.assertIn('[-1e9999, ' + str(ts - 1) + ']', response.content)
108114
data = json.loads(response.content)
109-
end = data[0]['datapoints'][-4:]
115+
end = data[0]['datapoints'][-7:]
110116
self.assertEqual(
111-
end, [[None, ts - 3], [0.12345678901234568, ts - 2], [0.4, ts - 1], [0.6, ts]])
117+
end, [[None, ts - 6],
118+
[0.12345678901234568, ts - 5],
119+
[0.4, ts - 4],
120+
[0.6, ts - 3],
121+
[float('inf'), ts - 2],
122+
[float('-inf'), ts - 1],
123+
[None, ts]])
112124

113125
response = self.client.get(url, {'target': 'test', 'format': 'dygraph'})
126+
self.assertIn('[' + str((ts - 2) * 1000) + ', Infinity]', response.content)
127+
self.assertIn('[' + str((ts - 1) * 1000) + ', -Infinity]', response.content)
114128
data = json.loads(response.content)
115-
end = data['data'][-4:]
129+
end = data['data'][-7:]
116130
self.assertEqual(end,
117-
[[(ts - 3) * 1000, None],
118-
[(ts - 2) * 1000, 0.123456789012],
119-
[(ts - 1) * 1000, 0.4],
120-
[ts * 1000, 0.6]])
131+
[[(ts - 6) * 1000, None],
132+
[(ts - 5) * 1000, 0.123456789012],
133+
[(ts - 4) * 1000, 0.4],
134+
[(ts - 3) * 1000, 0.6],
135+
[(ts - 2) * 1000, float('inf')],
136+
[(ts - 1) * 1000, float('-inf')],
137+
[ts * 1000, None]])
121138

122139
response = self.client.get(url, {'target': 'test', 'format': 'rickshaw'})
123140
data = json.loads(response.content)
124-
end = data[0]['datapoints'][-4:]
141+
end = data[0]['datapoints'][-7:-1]
125142
self.assertEqual(end,
126-
[{'x': ts - 3, 'y': None},
127-
{'x': ts - 2, 'y': 0.12345678901234568},
128-
{'x': ts - 1, 'y': 0.4},
129-
{'x': ts, 'y': 0.6}])
143+
[{'x': ts - 6, 'y': None},
144+
{'x': ts - 5, 'y': 0.12345678901234568},
145+
{'x': ts - 4, 'y': 0.4},
146+
{'x': ts - 3, 'y': 0.6},
147+
{'x': ts - 2, 'y': float('inf')},
148+
{'x': ts - 1, 'y': float('-inf')}])
149+
150+
last = data[0]['datapoints'][-1]
151+
self.assertEqual(last['x'], ts)
152+
self.assertTrue(math.isnan(last['y']))
153+
130154

131155
def test_hash_request(self):
132156
# Requests with the same parameters should hash to the same values,

0 commit comments

Comments
 (0)