Skip to content

Commit

Permalink
feat(exporter-prometheus): add additional attributes option (#5317)
Browse files Browse the repository at this point in the history
Co-authored-by: Marc Pichler <[email protected]>
  • Loading branch information
marius-a-mueller and pichlermarc authored Feb 13, 2025
1 parent 60f2ce9 commit 7a1e1b2
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 7 deletions.
2 changes: 2 additions & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ All notable changes to experimental packages in this project will be documented
* feat(instrumentation-fetch): add a `requestHook` option [#5380](https://github.com/open-telemetry/opentelemetry-js/pull/5380)
* feat(instrumentation): re-export initialize function from import-in-the-middle [#5123](https://github.com/open-telemetry/opentelemetry-js/pull/5123)
* feat(sdk-node): lower diagnostic level [#5360](https://github.com/open-telemetry/opentelemetry-js/pull/5360) @cjihrig
* feat(exporter-prometheus): add additional attributes option [#5317](https://github.com/open-telemetry/opentelemetry-js/pull/5317) @marius-a-mueller
* Add `withResourceConstantLabels` option to `ExporterConfig`. It can be used to define a regex pattern to choose which resource attributes will be used as static labels on the metrics. The default is to not set any static labels.

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class PrometheusExporter extends MetricReader {
endpoint: '/metrics',
prefix: '',
appendTimestamp: false,
withResourceConstantLabels: undefined,
};

private readonly _host?: string;
Expand Down Expand Up @@ -82,11 +83,15 @@ export class PrometheusExporter extends MetricReader {
typeof config.appendTimestamp === 'boolean'
? config.appendTimestamp
: PrometheusExporter.DEFAULT_OPTIONS.appendTimestamp;
const _withResourceConstantLabels =
config.withResourceConstantLabels ||
PrometheusExporter.DEFAULT_OPTIONS.withResourceConstantLabels;
// unref to prevent prometheus exporter from holding the process open on exit
this._server = createServer(this._requestHandler).unref();
this._serializer = new PrometheusSerializer(
this._prefix,
this._appendTimestamp
this._appendTimestamp,
_withResourceConstantLabels
);

this._baseUrl = `http://${this._host}:${this._port}/`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,29 @@ const NO_REGISTERED_METRICS = '# no registered metrics';
export class PrometheusSerializer {
private _prefix: string | undefined;
private _appendTimestamp: boolean;
private _additionalAttributes: Attributes | undefined;
private _withResourceConstantLabels: RegExp | undefined;

constructor(prefix?: string, appendTimestamp = false) {
constructor(
prefix?: string,
appendTimestamp = false,
withResourceConstantLabels?: RegExp
) {
if (prefix) {
this._prefix = prefix + '_';
}
this._appendTimestamp = appendTimestamp;
this._withResourceConstantLabels = withResourceConstantLabels;
}

serialize(resourceMetrics: ResourceMetrics): string {
let str = '';

this._additionalAttributes = this._filterResourceConstantLabels(
resourceMetrics.resource.attributes,
this._withResourceConstantLabels
);

for (const scopeMetrics of resourceMetrics.scopeMetrics) {
str += this._serializeScopeMetrics(scopeMetrics);
}
Expand All @@ -193,6 +205,22 @@ export class PrometheusSerializer {
return this._serializeResource(resourceMetrics.resource) + str;
}

private _filterResourceConstantLabels(
attributes: Attributes,
pattern: RegExp | undefined
) {
if (pattern) {
const filteredAttributes: Attributes = {};
for (const [key, value] of Object.entries(attributes)) {
if (key.match(pattern)) {
filteredAttributes[key] = value;
}
}
return filteredAttributes;
}
return;
}

private _serializeScopeMetrics(scopeMetrics: ScopeMetrics) {
let str = '';
for (const metric of scopeMetrics.metrics) {
Expand Down Expand Up @@ -260,7 +288,7 @@ export class PrometheusSerializer {
attributes,
value,
this._appendTimestamp ? timestamp : undefined,
undefined
this._additionalAttributes
);
return results;
}
Expand All @@ -285,7 +313,7 @@ export class PrometheusSerializer {
attributes,
value,
this._appendTimestamp ? timestamp : undefined,
undefined
this._additionalAttributes
);
}

Expand All @@ -312,12 +340,12 @@ export class PrometheusSerializer {
attributes,
cumulativeSum,
this._appendTimestamp ? timestamp : undefined,
{
Object.assign({}, this._additionalAttributes ?? {}, {
le:
upperBound === undefined || upperBound === Infinity
? '+Inf'
: String(upperBound),
}
})
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,12 @@ export interface ExporterConfig {
* @experimental
*/
metricProducers?: MetricProducer[];

/**
* Regex pattern for defining which resource attributes will be applied
* as constant labels to the metrics.
* e.g. 'telemetry_.+' for all attributes starting with 'telemetry'.
* @default undefined (no resource attributes are applied)
*/
withResourceConstantLabels?: RegExp;
}
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,40 @@ describe('PrometheusExporter', () => {
'',
]);
});

it('should export a metric with all resource attributes', async () => {
exporter = new PrometheusExporter({
withResourceConstantLabels: /.*/,
});
setup(exporter);
const body = await request('http://localhost:9464/metrics');
const lines = body.split('\n');

assert.deepStrictEqual(lines, [
...serializedDefaultResourceLines,
'# HELP counter_total description missing',
'# TYPE counter_total counter',
`counter_total{key1="attributeValue1",service_name="${serviceName}",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}",telemetry_sdk_version="${sdkVersion}"} 10`,
'',
]);
});

it('should export a metric with two resource attributes', async () => {
exporter = new PrometheusExporter({
withResourceConstantLabels: /telemetry.sdk.language|telemetry.sdk.name/,
});
setup(exporter);
const body = await request('http://localhost:9464/metrics');
const lines = body.split('\n');

assert.deepStrictEqual(lines, [
...serializedDefaultResourceLines,
'# HELP counter_total description missing',
'# TYPE counter_total counter',
`counter_total{key1="attributeValue1",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}"} 10`,
'',
]);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ const attributes = {
foo2: 'bar2',
};

const resourceAttributes = `service_name="${serviceName}",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}",telemetry_sdk_version="${sdkVersion}"`;

const serializedDefaultResource =
'# HELP target_info Target metadata\n' +
'# TYPE target_info gauge\n' +
`target_info{service_name="${serviceName}",telemetry_sdk_language="${sdkLanguage}",telemetry_sdk_name="${sdkName}",telemetry_sdk_version="${sdkVersion}"} 1\n`;
`target_info{${resourceAttributes}} 1\n`;

class TestMetricReader extends MetricReader {
constructor() {
Expand Down Expand Up @@ -104,6 +106,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(metric.dataPointType, DataPointType.SUM);
const pointData = metric.dataPoints as DataPoint<number>[];
assert.strictEqual(pointData.length, 1);
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeSingularDataPoint'](
metric.descriptor.name,
Expand Down Expand Up @@ -159,6 +165,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(metric.dataPointType, DataPointType.HISTOGRAM);
const pointData = metric.dataPoints as DataPoint<Histogram>[];
assert.strictEqual(pointData.length, 1);
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeHistogramDataPoint'](
metric.descriptor.name,
Expand Down Expand Up @@ -195,6 +205,20 @@ describe('PrometheusSerializer', () => {
`test_bucket{foo1="bar1",foo2="bar2",le="+Inf"} 1 ${mockedHrTimeMs}\n`
);
});

it('serialize metric record with sum aggregator with timestamp and all resource attributes', async () => {
const serializer = new PrometheusSerializer(undefined, true, /.*/);
const result = await testSerializer(serializer);
assert.strictEqual(
result,
`test_count{foo1="bar1",foo2="bar2",${resourceAttributes}} 1 ${mockedHrTimeMs}\n` +
`test_sum{foo1="bar1",foo2="bar2",${resourceAttributes}} 5 ${mockedHrTimeMs}\n` +
`test_bucket{foo1="bar1",foo2="bar2",${resourceAttributes},le="1"} 0 ${mockedHrTimeMs}\n` +
`test_bucket{foo1="bar1",foo2="bar2",${resourceAttributes},le="10"} 1 ${mockedHrTimeMs}\n` +
`test_bucket{foo1="bar1",foo2="bar2",${resourceAttributes},le="100"} 1 ${mockedHrTimeMs}\n` +
`test_bucket{foo1="bar1",foo2="bar2",${resourceAttributes},le="+Inf"} 1 ${mockedHrTimeMs}\n`
);
});
});
});

Expand Down Expand Up @@ -224,6 +248,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
const scopeMetrics = resourceMetrics.scopeMetrics[0];
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeScopeMetrics'](scopeMetrics);
return result;
Expand Down Expand Up @@ -252,6 +280,18 @@ describe('PrometheusSerializer', () => {
`test_total{val="2"} 1 ${mockedHrTimeMs}\n`
);
});

it('should serialize metric record with timestamp and all resource attributes', async () => {
const serializer = new PrometheusSerializer(undefined, true, /.*/);
const result = await testSerializer(serializer);
assert.strictEqual(
result,
'# HELP test_total foobar\n' +
'# TYPE test_total counter\n' +
`test_total{val="1",${resourceAttributes}} 1 ${mockedHrTimeMs}\n` +
`test_total{val="2",${resourceAttributes}} 1 ${mockedHrTimeMs}\n`
);
});
});

describe('non-monotonic Sum', () => {
Expand Down Expand Up @@ -279,6 +319,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
const scopeMetrics = resourceMetrics.scopeMetrics[0];
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

return serializer['_serializeScopeMetrics'](scopeMetrics);
}
Expand Down Expand Up @@ -306,6 +350,19 @@ describe('PrometheusSerializer', () => {
`test_total{val="2"} 1 ${mockedHrTimeMs}\n`
);
});

it('serialize metric record with timestamp and all resource attributes', async () => {
const serializer = new PrometheusSerializer(undefined, true, /.*/);

const result = await testSerializer(serializer);
assert.strictEqual(
result,
'# HELP test_total foobar\n' +
'# TYPE test_total gauge\n' +
`test_total{val="1",${resourceAttributes}} 1 ${mockedHrTimeMs}\n` +
`test_total{val="2",${resourceAttributes}} 1 ${mockedHrTimeMs}\n`
);
});
});

describe('Gauge', () => {
Expand Down Expand Up @@ -335,6 +392,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
const scopeMetrics = resourceMetrics.scopeMetrics[0];
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

return serializer['_serializeScopeMetrics'](scopeMetrics);
}
Expand Down Expand Up @@ -362,6 +423,18 @@ describe('PrometheusSerializer', () => {
`test_total{val="2"} 1 ${mockedHrTimeMs}\n`
);
});

it('serialize metric record with timestamp and all resource attributes', async () => {
const serializer = new PrometheusSerializer(undefined, true, /.*/);
const result = await testSerializer(serializer);
assert.strictEqual(
result,
'# HELP test_total foobar\n' +
'# TYPE test_total gauge\n' +
`test_total{val="1",${resourceAttributes}} 1 ${mockedHrTimeMs}\n` +
`test_total{val="2",${resourceAttributes}} 1 ${mockedHrTimeMs}\n`
);
});
});

describe('with ExplicitBucketHistogramAggregation', () => {
Expand Down Expand Up @@ -395,6 +468,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
const scopeMetrics = resourceMetrics.scopeMetrics[0];
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeScopeMetrics'](scopeMetrics);
return result;
Expand Down Expand Up @@ -422,6 +499,28 @@ describe('PrometheusSerializer', () => {
);
});

it('serialize cumulative metric record with all resource attributes', async () => {
const serializer = new PrometheusSerializer('', false, /.*/);
const result = await testSerializer(serializer);
assert.strictEqual(
result,
'# HELP test foobar\n' +
'# TYPE test histogram\n' +
`test_count{val="1",${resourceAttributes}} 3\n` +
`test_sum{val="1",${resourceAttributes}} 175\n` +
`test_bucket{val="1",${resourceAttributes},le="1"} 0\n` +
`test_bucket{val="1",${resourceAttributes},le="10"} 1\n` +
`test_bucket{val="1",${resourceAttributes},le="100"} 2\n` +
`test_bucket{val="1",${resourceAttributes},le="+Inf"} 3\n` +
`test_count{val="2",${resourceAttributes}} 1\n` +
`test_sum{val="2",${resourceAttributes}} 5\n` +
`test_bucket{val="2",${resourceAttributes},le="1"} 0\n` +
`test_bucket{val="2",${resourceAttributes},le="10"} 1\n` +
`test_bucket{val="2",${resourceAttributes},le="100"} 1\n` +
`test_bucket{val="2",${resourceAttributes},le="+Inf"} 1\n`
);
});

it('serialize cumulative metric record on instrument that allows negative values', async () => {
const serializer = new PrometheusSerializer();
const reader = new TestMetricReader();
Expand Down Expand Up @@ -453,6 +552,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(resourceMetrics.scopeMetrics.length, 1);
assert.strictEqual(resourceMetrics.scopeMetrics[0].metrics.length, 1);
const scopeMetrics = resourceMetrics.scopeMetrics[0];
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeScopeMetrics'](scopeMetrics);
assert.strictEqual(
Expand Down Expand Up @@ -504,6 +607,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(metric.dataPointType, DataPointType.SUM);
const pointData = metric.dataPoints as DataPoint<number>[];
assert.strictEqual(pointData.length, 1);
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

if (exportAll) {
const result = serializer.serialize(resourceMetrics);
Expand Down Expand Up @@ -595,6 +702,10 @@ describe('PrometheusSerializer', () => {
assert.strictEqual(metric.dataPointType, DataPointType.SUM);
const pointData = metric.dataPoints as DataPoint<number>[];
assert.strictEqual(pointData.length, 1);
const resourceAttributes = resourceMetrics.resource.attributes;
serializer['_additionalAttributes'] = serializer[
'_filterResourceConstantLabels'
](resourceAttributes, serializer['_withResourceConstantLabels']);

const result = serializer['_serializeSingularDataPoint'](
metric.descriptor.name,
Expand Down

0 comments on commit 7a1e1b2

Please sign in to comment.