Skip to content

Commit a38aa2b

Browse files
authored
perf: improve performance of rendering metrics to Prometheus string (#549)
1 parent 0f872ff commit a38aa2b

File tree

4 files changed

+66
-17
lines changed

4 files changed

+66
-17
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
1313

1414
### Changed
1515

16+
- Refactor histogram internals and provide a fast path for rendering metrics to
17+
Prometheus strings when there are many labels shared across different values.
1618
- Disable custom content encoding for pushgateway delete requests in order to
1719
avoid failures from the server when using `Content-Encoding: gzip` header.
1820
- Refactor `escapeString` helper in `lib/registry.js` to improve performance and

lib/histogram.js

+33-10
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class Histogram extends Metric {
9797
}
9898

9999
async get() {
100+
const data = await this.getForPromString();
101+
data.values = data.values.map(splayLabels);
102+
return data;
103+
}
104+
105+
async getForPromString() {
100106
if (this.collect) {
101107
const v = this.collect();
102108
if (v instanceof Promise) await v;
@@ -176,9 +182,10 @@ function startTimer(startLabels) {
176182
};
177183
}
178184

179-
function setValuePair(labels, value, metricName, exemplar) {
185+
function setValuePair(labels, value, metricName, exemplar, sharedLabels = {}) {
180186
return {
181187
labels,
188+
sharedLabels,
182189
value,
183190
metricName,
184191
exemplar,
@@ -255,20 +262,17 @@ function convertLabelsAndValues(labels, value) {
255262
function extractBucketValuesForExport(histogram) {
256263
return bucketData => {
257264
const buckets = [];
258-
const bucketLabelNames = Object.keys(bucketData.labels);
259265
let acc = 0;
260266
for (const upperBound of histogram.upperBounds) {
261267
acc += bucketData.bucketValues[upperBound];
262268
const lbls = { le: upperBound };
263-
for (const labelName of bucketLabelNames) {
264-
lbls[labelName] = bucketData.labels[labelName];
265-
}
266269
buckets.push(
267270
setValuePair(
268271
lbls,
269272
acc,
270273
`${histogram.name}_bucket`,
271274
bucketData.bucketExemplars[upperBound],
275+
bucketData.labels,
272276
),
273277
);
274278
}
@@ -281,21 +285,40 @@ function addSumAndCountForExport(histogram) {
281285
acc.push(...d.buckets);
282286

283287
const infLabel = { le: '+Inf' };
284-
for (const label of Object.keys(d.data.labels)) {
285-
infLabel[label] = d.data.labels[label];
286-
}
287288
acc.push(
288289
setValuePair(
289290
infLabel,
290291
d.data.count,
291292
`${histogram.name}_bucket`,
292293
d.data.bucketExemplars['-1'],
294+
d.data.labels,
295+
),
296+
setValuePair(
297+
{},
298+
d.data.sum,
299+
`${histogram.name}_sum`,
300+
undefined,
301+
d.data.labels,
302+
),
303+
setValuePair(
304+
{},
305+
d.data.count,
306+
`${histogram.name}_count`,
307+
undefined,
308+
d.data.labels,
293309
),
294-
setValuePair(d.data.labels, d.data.sum, `${histogram.name}_sum`),
295-
setValuePair(d.data.labels, d.data.count, `${histogram.name}_count`),
296310
);
297311
return acc;
298312
};
299313
}
300314

315+
function splayLabels(bucket) {
316+
const { sharedLabels, labels, ...newBucket } = bucket;
317+
for (const label of Object.keys(sharedLabels)) {
318+
labels[label] = sharedLabels[label];
319+
}
320+
newBucket.labels = labels;
321+
return newBucket;
322+
}
323+
301324
module.exports = Histogram;

lib/registry.js

+29-5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class Registry {
2929
}
3030

3131
async getMetricsAsString(metrics) {
32-
const metric = await metrics.get();
32+
const metric =
33+
typeof metrics.getForPromString === 'function'
34+
? await metrics.getForPromString()
35+
: await metrics.get();
3336

3437
const name = escapeString(metric.name);
3538
const help = `# HELP ${name} ${escapeString(metric.help)}`;
@@ -41,6 +44,7 @@ class Registry {
4144

4245
for (const val of metric.values || []) {
4346
let { metricName = name, labels = {} } = val;
47+
const { sharedLabels = {} } = val;
4448
if (
4549
this.contentType === Registry.OPENMETRICS_CONTENT_TYPE &&
4650
metric.type === 'counter'
@@ -52,11 +56,19 @@ class Registry {
5256
labels = { ...labels, ...defaultLabels, ...labels };
5357
}
5458

55-
const formattedLabels = formatLabels(labels);
56-
const labelsString = formattedLabels.length
57-
? `{${formattedLabels.join(',')}}`
58-
: '';
59+
// We have to flatten these separately to avoid duplicate labels appearing
60+
// between the base labels and the shared labels
61+
const formattedLabels = [];
62+
for (const [n, v] of Object.entries(labels)) {
63+
if (Object.prototype.hasOwnProperty.call(sharedLabels, n)) {
64+
continue;
65+
}
66+
formattedLabels.push(`${n}="${escapeLabelValue(v)}"`);
67+
}
5968

69+
const flattenedShared = flattenSharedLabels(sharedLabels);
70+
const labelParts = [...formattedLabels, flattenedShared].filter(Boolean);
71+
const labelsString = labelParts.length ? `{${labelParts.join(',')}}` : '';
6072
values.push(
6173
`${metricName}${labelsString} ${getValueAsString(val.value)}`,
6274
);
@@ -207,6 +219,18 @@ function formatLabels(labels) {
207219
);
208220
}
209221

222+
const sharedLabelCache = new WeakMap();
223+
function flattenSharedLabels(labels) {
224+
const cached = sharedLabelCache.get(labels);
225+
if (cached) {
226+
return cached;
227+
}
228+
229+
const formattedLabels = formatLabels(labels);
230+
const flattened = formattedLabels.join(',');
231+
sharedLabelCache.set(labels, flattened);
232+
return flattened;
233+
}
210234
function escapeLabelValue(str) {
211235
if (typeof str !== 'string') {
212236
return str;

test/registerTest.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -505,15 +505,15 @@ describe('Register', () => {
505505
const metrics = await r.metrics();
506506
const lines = metrics.split('\n');
507507
expect(lines).toContain(
508-
'my_histogram_bucket{le="1",type="myType",env="development"} 1',
508+
'my_histogram_bucket{le="1",env="development",type="myType"} 1',
509509
);
510510

511511
myHist.observe(1);
512512

513513
const metrics2 = await r.metrics();
514514
const lines2 = metrics2.split('\n');
515515
expect(lines2).toContain(
516-
'my_histogram_bucket{le="1",type="myType",env="development"} 2',
516+
'my_histogram_bucket{le="1",env="development",type="myType"} 2',
517517
);
518518
});
519519
});

0 commit comments

Comments
 (0)