Skip to content

Commit

Permalink
perf: improve performance of rendering metrics to Prometheus string (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ngavalas committed Mar 12, 2023
1 parent 0f872ff commit a38aa2b
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 17 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/).

### Changed

- Refactor histogram internals and provide a fast path for rendering metrics to
Prometheus strings when there are many labels shared across different values.
- Disable custom content encoding for pushgateway delete requests in order to
avoid failures from the server when using `Content-Encoding: gzip` header.
- Refactor `escapeString` helper in `lib/registry.js` to improve performance and
Expand Down
43 changes: 33 additions & 10 deletions lib/histogram.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ class Histogram extends Metric {
}

async get() {
const data = await this.getForPromString();
data.values = data.values.map(splayLabels);
return data;
}

async getForPromString() {
if (this.collect) {
const v = this.collect();
if (v instanceof Promise) await v;
Expand Down Expand Up @@ -176,9 +182,10 @@ function startTimer(startLabels) {
};
}

function setValuePair(labels, value, metricName, exemplar) {
function setValuePair(labels, value, metricName, exemplar, sharedLabels = {}) {
return {
labels,
sharedLabels,
value,
metricName,
exemplar,
Expand Down Expand Up @@ -255,20 +262,17 @@ function convertLabelsAndValues(labels, value) {
function extractBucketValuesForExport(histogram) {
return bucketData => {
const buckets = [];
const bucketLabelNames = Object.keys(bucketData.labels);
let acc = 0;
for (const upperBound of histogram.upperBounds) {
acc += bucketData.bucketValues[upperBound];
const lbls = { le: upperBound };
for (const labelName of bucketLabelNames) {
lbls[labelName] = bucketData.labels[labelName];
}
buckets.push(
setValuePair(
lbls,
acc,
`${histogram.name}_bucket`,
bucketData.bucketExemplars[upperBound],
bucketData.labels,
),
);
}
Expand All @@ -281,21 +285,40 @@ function addSumAndCountForExport(histogram) {
acc.push(...d.buckets);

const infLabel = { le: '+Inf' };
for (const label of Object.keys(d.data.labels)) {
infLabel[label] = d.data.labels[label];
}
acc.push(
setValuePair(
infLabel,
d.data.count,
`${histogram.name}_bucket`,
d.data.bucketExemplars['-1'],
d.data.labels,
),
setValuePair(
{},
d.data.sum,
`${histogram.name}_sum`,
undefined,
d.data.labels,
),
setValuePair(
{},
d.data.count,
`${histogram.name}_count`,
undefined,
d.data.labels,
),
setValuePair(d.data.labels, d.data.sum, `${histogram.name}_sum`),
setValuePair(d.data.labels, d.data.count, `${histogram.name}_count`),
);
return acc;
};
}

function splayLabels(bucket) {
const { sharedLabels, labels, ...newBucket } = bucket;
for (const label of Object.keys(sharedLabels)) {
labels[label] = sharedLabels[label];
}
newBucket.labels = labels;
return newBucket;
}

module.exports = Histogram;
34 changes: 29 additions & 5 deletions lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ class Registry {
}

async getMetricsAsString(metrics) {
const metric = await metrics.get();
const metric =
typeof metrics.getForPromString === 'function'
? await metrics.getForPromString()
: await metrics.get();

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

for (const val of metric.values || []) {
let { metricName = name, labels = {} } = val;
const { sharedLabels = {} } = val;
if (
this.contentType === Registry.OPENMETRICS_CONTENT_TYPE &&
metric.type === 'counter'
Expand All @@ -52,11 +56,19 @@ class Registry {
labels = { ...labels, ...defaultLabels, ...labels };
}

const formattedLabels = formatLabels(labels);
const labelsString = formattedLabels.length
? `{${formattedLabels.join(',')}}`
: '';
// We have to flatten these separately to avoid duplicate labels appearing
// between the base labels and the shared labels
const formattedLabels = [];
for (const [n, v] of Object.entries(labels)) {
if (Object.prototype.hasOwnProperty.call(sharedLabels, n)) {
continue;
}
formattedLabels.push(`${n}="${escapeLabelValue(v)}"`);
}

const flattenedShared = flattenSharedLabels(sharedLabels);
const labelParts = [...formattedLabels, flattenedShared].filter(Boolean);
const labelsString = labelParts.length ? `{${labelParts.join(',')}}` : '';
values.push(
`${metricName}${labelsString} ${getValueAsString(val.value)}`,
);
Expand Down Expand Up @@ -207,6 +219,18 @@ function formatLabels(labels) {
);
}

const sharedLabelCache = new WeakMap();
function flattenSharedLabels(labels) {
const cached = sharedLabelCache.get(labels);
if (cached) {
return cached;
}

const formattedLabels = formatLabels(labels);
const flattened = formattedLabels.join(',');
sharedLabelCache.set(labels, flattened);
return flattened;
}
function escapeLabelValue(str) {
if (typeof str !== 'string') {
return str;
Expand Down
4 changes: 2 additions & 2 deletions test/registerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,15 +505,15 @@ describe('Register', () => {
const metrics = await r.metrics();
const lines = metrics.split('\n');
expect(lines).toContain(
'my_histogram_bucket{le="1",type="myType",env="development"} 1',
'my_histogram_bucket{le="1",env="development",type="myType"} 1',
);

myHist.observe(1);

const metrics2 = await r.metrics();
const lines2 = metrics2.split('\n');
expect(lines2).toContain(
'my_histogram_bucket{le="1",type="myType",env="development"} 2',
'my_histogram_bucket{le="1",env="development",type="myType"} 2',
);
});
});
Expand Down

0 comments on commit a38aa2b

Please sign in to comment.