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

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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