Skip to content

Commit a264ec0

Browse files
authored
Don't interleave histogram metrics in multi-process collector (#1148)
The OpenMetrics exposition format requires that samples for a given Metric (i.e. metric name and label set) are not interleaved, but the way that the multi-process collector handled accumulating histogram metrics could end up interleaving them. Restructure it slightly to guarantee that all the samples for a given Metric are kept together. Fixes: #1147 Signed-off-by: Colin Watson <[email protected]>
1 parent e8f8bae commit a264ec0

File tree

2 files changed

+94
-27
lines changed

2 files changed

+94
-27
lines changed

prometheus_client/multiprocess.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,32 +88,42 @@ def _parse_key(key):
8888
@staticmethod
8989
def _accumulate_metrics(metrics, accumulate):
9090
for metric in metrics.values():
91-
samples = defaultdict(float)
92-
sample_timestamps = defaultdict(float)
91+
samples = defaultdict(lambda: defaultdict(float))
92+
sample_timestamps = defaultdict(lambda: defaultdict(float))
9393
buckets = defaultdict(lambda: defaultdict(float))
94-
samples_setdefault = samples.setdefault
9594
for s in metric.samples:
9695
name, labels, value, timestamp, exemplar, native_histogram_value = s
96+
97+
if (
98+
metric.type == 'gauge'
99+
and metric._multiprocess_mode in (
100+
'min', 'livemin',
101+
'max', 'livemax',
102+
'sum', 'livesum',
103+
'mostrecent', 'livemostrecent',
104+
)
105+
):
106+
labels = tuple(l for l in labels if l[0] != 'pid')
107+
97108
if metric.type == 'gauge':
98-
without_pid_key = (name, tuple(l for l in labels if l[0] != 'pid'))
99109
if metric._multiprocess_mode in ('min', 'livemin'):
100-
current = samples_setdefault(without_pid_key, value)
110+
current = samples[labels].setdefault((name, labels), value)
101111
if value < current:
102-
samples[without_pid_key] = value
112+
samples[labels][(name, labels)] = value
103113
elif metric._multiprocess_mode in ('max', 'livemax'):
104-
current = samples_setdefault(without_pid_key, value)
114+
current = samples[labels].setdefault((name, labels), value)
105115
if value > current:
106-
samples[without_pid_key] = value
116+
samples[labels][(name, labels)] = value
107117
elif metric._multiprocess_mode in ('sum', 'livesum'):
108-
samples[without_pid_key] += value
118+
samples[labels][(name, labels)] += value
109119
elif metric._multiprocess_mode in ('mostrecent', 'livemostrecent'):
110-
current_timestamp = sample_timestamps[without_pid_key]
120+
current_timestamp = sample_timestamps[labels][name]
111121
timestamp = float(timestamp or 0)
112122
if current_timestamp < timestamp:
113-
samples[without_pid_key] = value
114-
sample_timestamps[without_pid_key] = timestamp
123+
samples[labels][(name, labels)] = value
124+
sample_timestamps[labels][name] = timestamp
115125
else: # all/liveall
116-
samples[(name, labels)] = value
126+
samples[labels][(name, labels)] = value
117127

118128
elif metric.type == 'histogram':
119129
# A for loop with early exit is faster than a genexpr
@@ -127,10 +137,10 @@ def _accumulate_metrics(metrics, accumulate):
127137
break
128138
else: # did not find the `le` key
129139
# _sum/_count
130-
samples[(name, labels)] += value
140+
samples[labels][(name, labels)] += value
131141
else:
132142
# Counter and Summary.
133-
samples[(name, labels)] += value
143+
samples[labels][(name, labels)] += value
134144

135145
# Accumulate bucket values.
136146
if metric.type == 'histogram':
@@ -143,14 +153,17 @@ def _accumulate_metrics(metrics, accumulate):
143153
)
144154
if accumulate:
145155
acc += value
146-
samples[sample_key] = acc
156+
samples[labels][sample_key] = acc
147157
else:
148-
samples[sample_key] = value
158+
samples[labels][sample_key] = value
149159
if accumulate:
150-
samples[(metric.name + '_count', labels)] = acc
160+
samples[labels][(metric.name + '_count', labels)] = acc
151161

152162
# Convert to correct sample format.
153-
metric.samples = [Sample(name_, dict(labels), value) for (name_, labels), value in samples.items()]
163+
metric.samples = []
164+
for _, samples_by_labels in samples.items():
165+
for (name_, labels), value in samples_by_labels.items():
166+
metric.samples.append(Sample(name_, dict(labels), value))
154167
return metrics.values()
155168

156169
def collect(self):

tests/test_multiprocess.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,8 @@ def add_label(key, value):
276276
Sample('g', add_label('pid', '1'), 1.0),
277277
])
278278

279-
metrics['h'].samples.sort(
280-
key=lambda x: (x[0], float(x[1].get('le', 0)))
281-
)
282279
expected_histogram = [
280+
Sample('h_sum', labels, 6.0),
283281
Sample('h_bucket', add_label('le', '0.005'), 0.0),
284282
Sample('h_bucket', add_label('le', '0.01'), 0.0),
285283
Sample('h_bucket', add_label('le', '0.025'), 0.0),
@@ -296,7 +294,66 @@ def add_label(key, value):
296294
Sample('h_bucket', add_label('le', '10.0'), 2.0),
297295
Sample('h_bucket', add_label('le', '+Inf'), 2.0),
298296
Sample('h_count', labels, 2.0),
299-
Sample('h_sum', labels, 6.0),
297+
]
298+
299+
self.assertEqual(metrics['h'].samples, expected_histogram)
300+
301+
def test_collect_histogram_ordering(self):
302+
pid = 0
303+
values.ValueClass = MultiProcessValue(lambda: pid)
304+
labels = {i: i for i in 'abcd'}
305+
306+
def add_label(key, value):
307+
l = labels.copy()
308+
l[key] = value
309+
return l
310+
311+
h = Histogram('h', 'help', labelnames=['view'], registry=None)
312+
313+
h.labels(view='view1').observe(1)
314+
315+
pid = 1
316+
317+
h.labels(view='view1').observe(5)
318+
h.labels(view='view2').observe(1)
319+
320+
metrics = {m.name: m for m in self.collector.collect()}
321+
322+
expected_histogram = [
323+
Sample('h_sum', {'view': 'view1'}, 6.0),
324+
Sample('h_bucket', {'view': 'view1', 'le': '0.005'}, 0.0),
325+
Sample('h_bucket', {'view': 'view1', 'le': '0.01'}, 0.0),
326+
Sample('h_bucket', {'view': 'view1', 'le': '0.025'}, 0.0),
327+
Sample('h_bucket', {'view': 'view1', 'le': '0.05'}, 0.0),
328+
Sample('h_bucket', {'view': 'view1', 'le': '0.075'}, 0.0),
329+
Sample('h_bucket', {'view': 'view1', 'le': '0.1'}, 0.0),
330+
Sample('h_bucket', {'view': 'view1', 'le': '0.25'}, 0.0),
331+
Sample('h_bucket', {'view': 'view1', 'le': '0.5'}, 0.0),
332+
Sample('h_bucket', {'view': 'view1', 'le': '0.75'}, 0.0),
333+
Sample('h_bucket', {'view': 'view1', 'le': '1.0'}, 1.0),
334+
Sample('h_bucket', {'view': 'view1', 'le': '2.5'}, 1.0),
335+
Sample('h_bucket', {'view': 'view1', 'le': '5.0'}, 2.0),
336+
Sample('h_bucket', {'view': 'view1', 'le': '7.5'}, 2.0),
337+
Sample('h_bucket', {'view': 'view1', 'le': '10.0'}, 2.0),
338+
Sample('h_bucket', {'view': 'view1', 'le': '+Inf'}, 2.0),
339+
Sample('h_count', {'view': 'view1'}, 2.0),
340+
Sample('h_sum', {'view': 'view2'}, 1.0),
341+
Sample('h_bucket', {'view': 'view2', 'le': '0.005'}, 0.0),
342+
Sample('h_bucket', {'view': 'view2', 'le': '0.01'}, 0.0),
343+
Sample('h_bucket', {'view': 'view2', 'le': '0.025'}, 0.0),
344+
Sample('h_bucket', {'view': 'view2', 'le': '0.05'}, 0.0),
345+
Sample('h_bucket', {'view': 'view2', 'le': '0.075'}, 0.0),
346+
Sample('h_bucket', {'view': 'view2', 'le': '0.1'}, 0.0),
347+
Sample('h_bucket', {'view': 'view2', 'le': '0.25'}, 0.0),
348+
Sample('h_bucket', {'view': 'view2', 'le': '0.5'}, 0.0),
349+
Sample('h_bucket', {'view': 'view2', 'le': '0.75'}, 0.0),
350+
Sample('h_bucket', {'view': 'view2', 'le': '1.0'}, 1.0),
351+
Sample('h_bucket', {'view': 'view2', 'le': '2.5'}, 1.0),
352+
Sample('h_bucket', {'view': 'view2', 'le': '5.0'}, 1.0),
353+
Sample('h_bucket', {'view': 'view2', 'le': '7.5'}, 1.0),
354+
Sample('h_bucket', {'view': 'view2', 'le': '10.0'}, 1.0),
355+
Sample('h_bucket', {'view': 'view2', 'le': '+Inf'}, 1.0),
356+
Sample('h_count', {'view': 'view2'}, 1.0),
300357
]
301358

302359
self.assertEqual(metrics['h'].samples, expected_histogram)
@@ -347,10 +404,8 @@ def add_label(key, value):
347404
m.name: m for m in self.collector.merge(files, accumulate=False)
348405
}
349406

350-
metrics['h'].samples.sort(
351-
key=lambda x: (x[0], float(x[1].get('le', 0)))
352-
)
353407
expected_histogram = [
408+
Sample('h_sum', labels, 6.0),
354409
Sample('h_bucket', add_label('le', '0.005'), 0.0),
355410
Sample('h_bucket', add_label('le', '0.01'), 0.0),
356411
Sample('h_bucket', add_label('le', '0.025'), 0.0),
@@ -366,7 +421,6 @@ def add_label(key, value):
366421
Sample('h_bucket', add_label('le', '7.5'), 0.0),
367422
Sample('h_bucket', add_label('le', '10.0'), 0.0),
368423
Sample('h_bucket', add_label('le', '+Inf'), 0.0),
369-
Sample('h_sum', labels, 6.0),
370424
]
371425

372426
self.assertEqual(metrics['h'].samples, expected_histogram)

0 commit comments

Comments
 (0)