Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ This release marks our first release under the Prometheus umbrella.
- Graceful shutdown support, and data retention past worker termination
- Export `MetricObject`, `MetricObjectWithValues`, `MetricValue` and `MetricValueWithName` from the TypeScript definitions
- chore: Old label processing code marked as deprecated
- perf: Stop rebuilding the label array for every rendered series, and skip shared label handling entirely for values that do not set it; 9-22% faster `metrics()` in the registry benchmarks

### Added

Expand Down
35 changes: 20 additions & 15 deletions lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,34 @@ class Registry {
this.contentType === Registry.OPENMETRICS_CONTENT_TYPE;

for (const val of metric.values || []) {
let { metricName = name, labels = {} } = val;
const { sharedLabels = {} } = val;
if (isOpenMetrics && metric.type === 'counter') {
metricName = `${metricName}_total`;
}

const { metricName = name, labels = {}, sharedLabels } = val;
const seriesName =
isOpenMetrics && metric.type === 'counter'
? `${metricName}_total`
: metricName;

// Make a copy before mutating
const seriesLabels =
defaultLabelNames === undefined ? labels : { ...labels };
if (defaultLabelNames !== undefined) {
// Make a copy before mutating
labels = { ...labels };

for (const labelName of defaultLabelNames) {
labels[labelName] ??= this._defaultLabels[labelName];
seriesLabels[labelName] ??= this._defaultLabels[labelName];
}
}

// We have to flatten these separately to avoid duplicate labels appearing
// between the base labels and the shared labels
const formattedLabels = formatLabels(labels, sharedLabels);

const flattenedShared = flattenSharedLabels(sharedLabels);
const labelParts = [...formattedLabels, flattenedShared].filter(Boolean);
const labelParts = formatLabels(seriesLabels, sharedLabels);
if (sharedLabels !== undefined) {
// A histogram declared without label names still shares an empty
// object, and appending its flattened form would emit `{le="1",}`
const flattenedShared = flattenSharedLabels(sharedLabels);
if (flattenedShared) {
labelParts.push(flattenedShared);
}
}
const labelsString = labelParts.length ? `{${labelParts.join(',')}}` : '';
let fullMetricLine = `${metricName}${labelsString} ${getValueAsString(
let fullMetricLine = `${seriesName}${labelsString} ${getValueAsString(
val.value,
)}`;

Expand Down
45 changes: 45 additions & 0 deletions test/registerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,51 @@ describe('Register', () => {
describe('Registry with default labels', () => {
const Registry = require('../lib/registry');

it('should not emit a label twice when a default label name is also a shared label', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this increase code coverage? I'm surprised we don't already have these tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statements and lines stay at 98.4%, branches go 96.72% to 98.48% on registry.js. The paths were already being executed, what was missing was any assertion on what they produce.

On main you can delete the dedup exclusion, or the escaping inside flattenSharedLabels(), and the suite is still 551 green. Dropping every shared label after the first is noticed by exactly one thing, an OpenMetrics snapshot in the exemplar tests.

// Histogram buckets carry the series labels as shared labels; a default
// label of the same name must not be emitted alongside them. `env` does
// not collide, so it has to survive the merge.
const r = new Registry(regType);
r.setDefaultLabels({ route: 'default-route', env: 'production' });

const histogram = new Histogram({
name: 'my_histogram',
help: 'my histogram',
registers: [r],
labelNames: ['method', 'route'],
buckets: [1],
});
histogram.observe({ method: 'a"b\nc\\d', route: 'actual-route' }, 0.5);

const metrics = await r.metrics();

expect(metrics.split('\n')).toContain(
'my_histogram_bucket{le="1",env="production",method="a\\"b\\nc\\\\d",route="actual-route"} 1',
);
expect(metrics).not.toContain('default-route');
});

it('should treat only an absent sharedLabels as having none', async () => {
// `null` still reaches Object.entries and throws, as it did before the
// fast path. A truthiness check would render the series without them.
const r = new Registry(regType);
const values = [{ value: 1, labels: { a: '1' }, sharedLabels: null }];
r.registerMetric({
name: 'shared_null',
help: 'shared_null',
get() {
return {
name: 'shared_null',
help: 'shared_null',
type: 'gauge',
values,
};
},
});

await expect(r.metrics()).rejects.toThrow(TypeError);
});

describe('mutation tests', () => {
describe('registry.metrics()', () => {
it('should not throw with default labels (counter)', async () => {
Expand Down
Loading