diff --git a/CHANGELOG.md b/CHANGELOG.md index f00f3a9d967..c22aa7230ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se ### :boom: Breaking Change -* feat(sdk-trace-base): Add `parentSpanContext` and remove `parentSpanId` from `Span` and `ReadableSpan` [#5450](https://github.com/open-telemetry/opentelemetry-js/pull/5450) @JacksonWeber +* feat(sdk-trace-base)!: Add `parentSpanContext` and remove `parentSpanId` from `Span` and `ReadableSpan` [#5450](https://github.com/open-telemetry/opentelemetry-js/pull/5450) @JacksonWeber + * (user-facing): the SDK's `Span`s `parentSpanId` was replaced by `parentSpanContext`, to migrate to the new property, please replace `span.parentSpanId` -> `span.parentSpanContext?.spanId` * feat(sdk-metrics)!: drop deprecated `type` field on `MetricDescriptor` [#5291](https://github.com/open-telemetry/opentelemetry-js/pull/5291) @chancancode * feat(sdk-metrics)!: drop deprecated `InstrumentDescriptor` type; use `MetricDescriptor` instead [#5277](https://github.com/open-telemetry/opentelemetry-js/pull/5266) @chancancode * feat(sdk-metrics)!: bump minimum version of `@opentelemetry/api` peer dependency to 1.9.0 [#5254](https://github.com/open-telemetry/opentelemetry-js/pull/5254) @chancancode diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 20db0e533d3..b96210f3d0e 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -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) diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts index e9f95051f82..83e7942b684 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusExporter.ts @@ -34,6 +34,7 @@ export class PrometheusExporter extends MetricReader { endpoint: '/metrics', prefix: '', appendTimestamp: false, + withResourceConstantLabels: undefined, }; private readonly _host?: string; @@ -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}/`; diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts index 04e2f5a6174..258fedc9788 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/PrometheusSerializer.ts @@ -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); } @@ -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) { @@ -260,7 +288,7 @@ export class PrometheusSerializer { attributes, value, this._appendTimestamp ? timestamp : undefined, - undefined + this._additionalAttributes ); return results; } @@ -285,7 +313,7 @@ export class PrometheusSerializer { attributes, value, this._appendTimestamp ? timestamp : undefined, - undefined + this._additionalAttributes ); } @@ -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), - } + }) ); } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/src/export/types.ts b/experimental/packages/opentelemetry-exporter-prometheus/src/export/types.ts index 78721a90c04..dbf5a15b595 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/src/export/types.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/src/export/types.ts @@ -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; } diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts index 6a31edbd827..93f6666ba2e 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusExporter.test.ts @@ -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`, + '', + ]); + }); }); }); diff --git a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts index 73ab372274a..3ec60cf19b8 100644 --- a/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts +++ b/experimental/packages/opentelemetry-exporter-prometheus/test/PrometheusSerializer.test.ts @@ -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() { @@ -104,6 +106,10 @@ describe('PrometheusSerializer', () => { assert.strictEqual(metric.dataPointType, DataPointType.SUM); const pointData = metric.dataPoints as DataPoint[]; assert.strictEqual(pointData.length, 1); + const resourceAttributes = resourceMetrics.resource.attributes; + serializer['_additionalAttributes'] = serializer[ + '_filterResourceConstantLabels' + ](resourceAttributes, serializer['_withResourceConstantLabels']); const result = serializer['_serializeSingularDataPoint']( metric.descriptor.name, @@ -159,6 +165,10 @@ describe('PrometheusSerializer', () => { assert.strictEqual(metric.dataPointType, DataPointType.HISTOGRAM); const pointData = metric.dataPoints as DataPoint[]; assert.strictEqual(pointData.length, 1); + const resourceAttributes = resourceMetrics.resource.attributes; + serializer['_additionalAttributes'] = serializer[ + '_filterResourceConstantLabels' + ](resourceAttributes, serializer['_withResourceConstantLabels']); const result = serializer['_serializeHistogramDataPoint']( metric.descriptor.name, @@ -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` + ); + }); }); }); @@ -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; @@ -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', () => { @@ -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); } @@ -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', () => { @@ -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); } @@ -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', () => { @@ -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; @@ -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(); @@ -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( @@ -504,6 +607,10 @@ describe('PrometheusSerializer', () => { assert.strictEqual(metric.dataPointType, DataPointType.SUM); const pointData = metric.dataPoints as DataPoint[]; assert.strictEqual(pointData.length, 1); + const resourceAttributes = resourceMetrics.resource.attributes; + serializer['_additionalAttributes'] = serializer[ + '_filterResourceConstantLabels' + ](resourceAttributes, serializer['_withResourceConstantLabels']); if (exportAll) { const result = serializer.serialize(resourceMetrics); @@ -595,6 +702,10 @@ describe('PrometheusSerializer', () => { assert.strictEqual(metric.dataPointType, DataPointType.SUM); const pointData = metric.dataPoints as DataPoint[]; assert.strictEqual(pointData.length, 1); + const resourceAttributes = resourceMetrics.resource.attributes; + serializer['_additionalAttributes'] = serializer[ + '_filterResourceConstantLabels' + ](resourceAttributes, serializer['_withResourceConstantLabels']); const result = serializer['_serializeSingularDataPoint']( metric.descriptor.name, diff --git a/packages/opentelemetry-context-zone-peer-dep/src/util.ts b/packages/opentelemetry-context-zone-peer-dep/src/util.ts index 7c4ac5f68d5..8f7634f48c7 100644 --- a/packages/opentelemetry-context-zone-peer-dep/src/util.ts +++ b/packages/opentelemetry-context-zone-peer-dep/src/util.ts @@ -15,13 +15,17 @@ */ /** - * check if an object has addEventListener and removeEventListener functions then it will return true. - * Generally only called with a `TargetWithEvents` but may be called with an unknown / any. + * check if an object has `addEventListener` and `removeEventListener` functions. + * Generally only called with a `TargetWithEvents` but may be called with an `unknown` value. * @param obj - The object to check. */ -export function isListenerObject(obj: any = {}): boolean { +export function isListenerObject(obj: unknown): boolean { return ( + typeof obj === 'object' && + obj !== null && + 'addEventListener' in obj && typeof obj.addEventListener === 'function' && + 'removeEventListener' in obj && typeof obj.removeEventListener === 'function' ); } diff --git a/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts b/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts index 06b71e1df5a..2fd0e8c493e 100644 --- a/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts +++ b/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts @@ -44,7 +44,7 @@ const resource = { 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': VERSION, - } + }, } as unknown as Resource; const parentSpanContext: api.SpanContext = { traceId: '',