8000 Add a text format parser. · Syncano/client_python@a2dae6c · GitHub
[go: up one dir, main page]

Skip to content

Commit a2dae6c

Browse files
committed
Add a text format parser.
Fix exposition of 'NaN' in text format (Python uses 'nan').
1 parent 854b401 commit a2dae6c

File tree

4 files changed

+435
-13
lines changed

4 files changed

+435
-13
lines changed

prometheus_client/bridge/graphite.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,19 @@ def __init__(self, address, registry=core.REGISTRY, timeout_seconds=30, _time=ti
5050
self._time = _time
5151

5252
def push(self):
53-
now = int(self._time.time())
54-
output = []
55-
for metric in self._registry.collect():
56-
for name, labels, value in metric.samples:
57-
if labels:
58-
labelstr = '.' + '.'.join(
59-
['{0}.{1}'.format(
60-
_sanitize(k), _sanitize(v))
61-
for k, v in sorted(labels.items())])
62-
else:
63-
labelstr = ''
64-
output.append('{0}{1} {2} {3}\n'.format(
65-
_sanitize(name), labelstr, float(value), now))
53+
now = int(self._time.time())
54+
output = []
55+
for metric in self._registry.collect():
56+
for name, labels, value in metric.samples:
57+
if labels:
58+
labelstr = '.' + '.'.join(
59+
['{0}.{1}'.format(
60+
_sanitize(k), _sanitize(v))
61+
for k, v in sorted(labels.items())])
62+
else:
63+
labelstr = ''
64+
output.append('{0}{1} {2} {3}\n'.format(
65+
_sanitize(name), labelstr, float(value), now))
6666

6767
conn = socket.create_connection(self._address, self._timeout)
6868
conn.sendall(''.join(output).encode('ascii'))

prometheus_client/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import unicode_literals
44

55
import copy
6+
import math
67
import re
78
import time
89
import types
@@ -95,6 +96,13 @@ def add_sample(self, name, labels, value):
9596
Internal-only, do not use.'''
9697
self.samples.append((name, labels, value))
9798

99+
def __eq__(self, other):
100+
return (isinstance(other, Metric)
101+
and self.name == other.name
102+
and self.documentation == other.documentation
103+
and self.type == other.type
104+
and self.samples == other.samples)
105+
98106

99107
class CounterMetricFamily(Metric):
100108
'''A single counter and its samples.
@@ -570,6 +578,8 @@ def _floatToGoString(d):
570578
return '+Inf'
571579
elif d == _MINUS_INF:
572580
return '-Inf'
581+
elif math.isnan(d):
582+
return 'NaN'
573583
else:
574584
return repr(float(d))
575585

prometheus_client/parser.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/bin/python
2+
3+
from __future__ import unicode_literals
4+
5+
try:
6+
import StringIO
7+
except ImportError:
8+
# Python 3
9+
import io as StringIO
10+
11+
from . import core
12+
13+
14+
def text_string_to_metric_families(text):
15+
"""Parse Prometheus text format from a string.
16+
17+
See text_fd_to_metric_families.
18+
"""
19+
for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
20+
yield metric_family
21+
22+
23+
def _unescape_help(text):
24+
result = []
25+
slash = False
26+
27+
for char in text:
28+
if slash:
29+
if char == '\\':
30+
result.append('\\')
31+
elif char == 'n':
32+
result.append('\n')
33+
else:
34+
result.append('\\' + char)
35+
slash = False
36+
else:
37+
if char == '\\':
38+
slash = True
39+
else:
40+
result.append(char)
41+
42+
if slash:
43+
result.append('\\')
44+
45+
return ''.join(result)
46+
47+
48+
def _parse_sample(text):
49+
name = []
50+
labelname = []
51+
labelvalue = []
52+
value = []
53+
labels = {}
54+
55+
state = 'name'
56+
57+
for char in text:
58+
if state == 'name':
59+
if char == '{':
60+
state = 'startoflabelname'
61+
elif char == ' ' or char == '\t':
62+
state = 'endofname'
63+
else:
64+
name.append(char)
65+
elif state == 'endofname':
66+
if char == ' ' or char == '\t':
67+
pass
68+
elif char == '{':
69+
state = 'startoflabelname'
70+
else:
71+
value.append(char)
72+
state = 'value'
73+
elif state == 'startoflabelname':
74+
if char == ' ' or char == '\t':
75+
pass
76+
elif char == '}':
77+
state = 'endoflabels'
78+
else:
79+
state = 'labelname'
80+
labelname.append(char)
81+
elif state == 'labelname':
82+
if char == '=':
83+
state = 'labelvaluequote'
84+
elif char == ' ' or char == '\t':
85+
state = 'labelvalueequals'
86+
else:
87+
labelname.append(char)
88+
elif state == 'labelvalueequals':
89+
if char == '=':
90+
state = 'labelvaluequote'
91+
elif char == ' ' or char == '\t':
92+
pass
93+
else:
94+
raise ValueError("Invalid line: " + text)
95+
elif state == 'labelvaluequote':
96+
if char == '"':
97+
state = 'labelvalue'
98+
elif char == ' ' or char == '\t':
99+
pass
100+
else:
101+
raise ValueError("Invalid line: " + text)
102+
elif state == 'labelvalue':
103+
if char == '\\':
104+
state = 'labelvalueslash'
105+
elif char == '"':
106+
labels[''.join(labelname)] = ''.join(labelvalue)
107+
labelname = []
108+
labelvalue = []
109+
state = 'nextlabel'
110+
else:
111+
labelvalue.append(char)
112+
elif state == 'labelvalueslash':
113+
state = 'labelvalue'
114+
if char == '\\':
115+
labelvalue.append('\\')
116+
elif char == 'n':
117+
labelvalue.append('\n')
118+
elif char == '"':
119+
labelvalue.append('"')
120+
else:
121+
labelvalue.append('\\' + char)
122+
elif state == 'nextlabel':
123+
if char == ',':
124+
state = 'labelname'
125+
elif char == '}':
126+
state = 'endoflabels'
127+
elif char == ' ' or char == '\t':
128+
pass
129+
else:
130+
raise ValueError("Invalid line: " + text)
131+
elif state == 'endoflabels':
132+
if char == ' ' or char == '\t':
133+
pass
134+
else:
135+
value.append(char)
136+
state = 'value'
137+
elif state == 'value':
138+
if char == ' ' or char == '\t':
139+
# Timestamps are not supported, halt
140+
break
141+
else:
142+
value.append(char)
143+
return (''.join(name), labels, float(''.join(value)))
144+
145+
146+
def text_fd_to_metric_families(fd):
147+
"""Parse Prometheus text format from a file descriptor.
148+
149+
This is a laxer parser than the main Go parser,
150+
so successful parsing does not imply that the parsed
151+
text meets the specification.
152+
153+
Yields core.Metric's.
154+
"""
155+
name = ''
156+
documentation = ''
157+
typ = 'untyped'
158+
samples = []
159+
allowed_names = []
160+
161+
def build_metric(name, documentation, typ, samples):
162+
metric = core.Metric(name, documentation, typ)
163+
metric.samples = samples
164+
return metric
165+
166+
for line in fd:
167+
line = line.strip()
168+
169+
if line.startswith('#'):
170+
parts = line.split(None, 3)
171+
if len(parts) < 2:
172+
continue
173+
if parts[1] == 'HELP':
174+
if parts[2] != name:
175+
if name != '':
176+
yield build_metric(name, documentation, typ, samples)
177+
# New metric
178+
name = parts[2]
179+
typ = 'untyped'
180+
samples = []
181+
allowed_names = [parts[2]]
182+
if len(parts) == 4:
183+
documentation = _unescape_help(parts[3])
184+
else:
185+
documentation = ''
186+
elif parts[1] == 'TYPE':
187+
if parts[2] != name:
188+
if name != '':
189+
yield build_metric(name, documentation, typ, samples)
190+
# New metric
191+
name = parts[2]
192+
documentation = ''
193+
samples = []
194+
typ = parts[3]
195+
allowed_names = {
196+
'counter': [''],
197+
'gauge': [''],
198+
'summary': ['_count', '_sum', ''],
199+
'histogram': ['_count', '_sum', '_bucket'],
200+
}.get(typ, [parts[2]])
201+
allowed_names = [name + n for n in allowed_names]
202+
else:
203+
# Ignore other comment tokens
204+
pass
205+
elif line == '':
206+
# Ignore blank lines
207+
pass
208+
else:
209+
sample = _parse_sample(line)
210+
if sample[0] not in allowed_names:
211+
if name != '':
212+
yield build_metric(name, documentation, typ, samples)
213+
# New metric, yield immediately as untyped singleton
214+
name = ''
215+
documentation = ''
216+
typ = 'untyped'
217+
samples = []
218+
allowed_names = []
219+
yield build_metric(sample[0], documentation, typ, [sample])
220+
else:
221+
samples.append(sample)
222+
223+
if name != '':
224+
yield build_metric(name, documentation, typ, samples)

0 commit comments

Comments
 (0)
0