From bad0c9f2515aa9bca0a95b1fd0a8c1fcaa89f341 Mon Sep 17 00:00:00 2001 From: Pratyush <116508117+pratstick@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:13:18 +0530 Subject: [PATCH 1/6] feat(api): add advisory attributes parameter to MetricAdvice Add optional attributes parameter to MetricAdvice interface to support advisory attribute filtering for metric instruments. This parameter allows users to specify which attribute keys should be kept when recording metrics, acting as an allow-list filter. The parameter is marked as experimental and follows the OpenTelemetry specification for advisory parameters. --- api/src/metrics/Metric.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/src/metrics/Metric.ts b/api/src/metrics/Metric.ts index b21aa7491fb..bee8aceeb42 100644 --- a/api/src/metrics/Metric.ts +++ b/api/src/metrics/Metric.ts @@ -30,6 +30,15 @@ export interface MetricAdvice { * aggregated with a HistogramAggregator. */ explicitBucketBoundaries?: number[]; + + /** + * @experimental + * An allow-list of attribute keys for this instrument. Only these keys will be kept + * on measurements recorded by this instrument. If not provided, all attributes are kept. + * + * See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-advisory-parameter-attributes + */ + attributes?: string[]; } /** From 09c03bd117c41c1513b0ec028facc9a96809a92e Mon Sep 17 00:00:00 2001 From: Pratyush <116508117+pratstick@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:13:41 +0530 Subject: [PATCH 2/6] feat(sdk-metrics): implement advisory attributes filtering Implement advisory attributes filtering in ViewRegistry by creating a default view with AllowListProcessor when no matching views are found and the instrument has advisory attributes specified. This ensures that only the specified attribute keys are retained when recording metrics, reducing cardinality and improving performance. --- packages/sdk-metrics/src/view/ViewRegistry.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/sdk-metrics/src/view/ViewRegistry.ts b/packages/sdk-metrics/src/view/ViewRegistry.ts index 5f4f367e920..9e0cf2c3198 100644 --- a/packages/sdk-metrics/src/view/ViewRegistry.ts +++ b/packages/sdk-metrics/src/view/ViewRegistry.ts @@ -19,6 +19,7 @@ import { InstrumentDescriptor } from '../InstrumentDescriptor'; import { InstrumentSelector } from './InstrumentSelector'; import { MeterSelector } from './MeterSelector'; import { View } from './View'; +import { createAllowListAttributesProcessor } from './AttributesProcessor'; export class ViewRegistry { private _registeredViews: View[] = []; @@ -38,9 +39,32 @@ export class ViewRegistry { ); }); + // If no registered views match, create a default view with instrument's advisory attributes + if (views.length === 0) { + const defaultView = this._createDefaultView(instrument); + return [defaultView]; + } + return views; } + private _createDefaultView(instrument: InstrumentDescriptor): View { + // Create default view with instrument's advisory attributes as an allow-list + const viewOptions: any = { + instrumentName: instrument.name, + instrumentType: instrument.type, + instrumentUnit: instrument.unit, + }; + + // If instrument has advisory attributes, use them as an allow-list + if (instrument.advice.attributes && instrument.advice.attributes.length > 0) { + viewOptions.attributesProcessors = [createAllowListAttributesProcessor(instrument.advice.attributes)]; + } + + // Create a view that matches this specific instrument + return new View(viewOptions); + } + private _matchInstrument( selector: InstrumentSelector, instrument: InstrumentDescriptor From 82948e168b2c08896743fdba1b66dcdad1fc2f46 Mon Sep 17 00:00:00 2001 From: Pratyush <116508117+pratstick@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:14:04 +0530 Subject: [PATCH 3/6] test(metrics): add tests for advisory attributes feature Add comprehensive unit and integration tests for the advisory attributes parameter functionality: - Unit tests for ViewRegistry advisory attributes handling - Integration tests covering various scenarios including edge cases - Tests for attribute filtering behavior with and without views - Verification of experimental feature marking --- .../advisory-attributes.integration.test.ts | 194 ++++++++++++++++++ .../test/view/ViewRegistry.test.ts | 131 ++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 packages/sdk-metrics/test/advisory-attributes.integration.test.ts diff --git a/packages/sdk-metrics/test/advisory-attributes.integration.test.ts b/packages/sdk-metrics/test/advisory-attributes.integration.test.ts new file mode 100644 index 00000000000..9156c2642c5 --- /dev/null +++ b/packages/sdk-metrics/test/advisory-attributes.integration.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import { MeterProvider } from '../src/MeterProvider'; +import { TestMetricReader } from './export/TestMetricReader'; +import { + defaultInstrumentationScope, + testResource, +} from './util'; + +describe('Advisory attributes parameter - Integration test', () => { + let meterProvider: MeterProvider; + let metricReader: TestMetricReader; + + beforeEach(() => { + metricReader = new TestMetricReader(); + meterProvider = new MeterProvider({ + resource: testResource, + readers: [metricReader], + }); + }); + + afterEach(() => { + metricReader.shutdown(); + }); + + it('should filter attributes based on advisory attributes parameter', async () => { + const meter = meterProvider.getMeter( + defaultInstrumentationScope.name, + defaultInstrumentationScope.version, + { + schemaUrl: defaultInstrumentationScope.schemaUrl, + } + ); + + // Create counter with advisory attributes + const counter = meter.createCounter('test_counter', { + description: 'Test counter with advisory attributes', + advice: { + attributes: ['allowed_key1', 'allowed_key2'], // @experimental + }, + }); + + // Record measurements with various attributes + counter.add(1, { + allowed_key1: 'value1', + allowed_key2: 'value2', + filtered_key: 'filtered_value', // This should be filtered out + }); + + counter.add(2, { + allowed_key1: 'value3', + another_filtered_key: 'another_filtered_value', // This should be filtered out + }); + + // Collect metrics + const metrics = await metricReader.collect(); + + assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1); + + const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0]; + assert.strictEqual(scopeMetrics.metrics.length, 1); + + const metric = scopeMetrics.metrics[0]; + assert.strictEqual(metric.descriptor.name, 'test_counter'); + assert.strictEqual(metric.dataPoints.length, 2); + + // Verify that only allowed attributes are present + const firstDataPoint = metric.dataPoints[0]; + assert.deepStrictEqual(firstDataPoint.attributes, { + allowed_key1: 'value1', + allowed_key2: 'value2', + }); + + const secondDataPoint = metric.dataPoints[1]; + assert.deepStrictEqual(secondDataPoint.attributes, { + allowed_key1: 'value3', + }); + }); + + it('should keep all attributes when no advisory attributes are specified', async () => { + const meter = meterProvider.getMeter( + defaultInstrumentationScope.name, + defaultInstrumentationScope.version, + { + schemaUrl: defaultInstrumentationScope.schemaUrl, + } + ); + + // Create counter without advisory attributes + const counter = meter.createCounter('test_counter_no_filter', { + description: 'Test counter without advisory attributes', + }); + + // Record measurements with various attributes + counter.add(1, { + key1: 'value1', + key2: 'value2', + key3: 'value3', + }); + + // Collect metrics + const metrics = await metricReader.collect(); + + assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1); + + const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0]; + assert.strictEqual(scopeMetrics.metrics.length, 1); + + const metric = scopeMetrics.metrics[0]; + assert.strictEqual(metric.descriptor.name, 'test_counter_no_filter'); + assert.strictEqual(metric.dataPoints.length, 1); + + // Verify that all attributes are present + const dataPoint = metric.dataPoints[0]; + assert.deepStrictEqual(dataPoint.attributes, { + key1: 'value1', + key2: 'value2', + key3: 'value3', + }); + }); + + it('should work with different instrument types', async () => { + const meter = meterProvider.getMeter( + defaultInstrumentationScope.name, + defaultInstrumentationScope.version, + { + schemaUrl: defaultInstrumentationScope.schemaUrl, + } + ); + + // Test with Histogram + const histogram = meter.createHistogram('test_histogram', { + description: 'Test histogram with advisory attributes', + advice: { + attributes: ['service'], // @experimental + }, + }); + + histogram.record(10, { + service: 'api', + endpoint: '/users', // This should be filtered out + }); + + // Test with UpDownCounter + const upDownCounter = meter.createUpDownCounter('test_updown', { + description: 'Test updown counter with advisory attributes', + advice: { + attributes: ['region'], // @experimental + }, + }); + + upDownCounter.add(5, { + region: 'us-west', + instance: 'i-12345', // This should be filtered out + }); + + // Collect metrics + const metrics = await metricReader.collect(); + + assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1); + + const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0]; + assert.strictEqual(scopeMetrics.metrics.length, 2); + + // Verify histogram filtering + const histogramMetric = scopeMetrics.metrics.find((m: any) => m.descriptor.name === 'test_histogram'); + assert.ok(histogramMetric); + assert.deepStrictEqual(histogramMetric.dataPoints[0].attributes, { + service: 'api', + }); + + // Verify updown counter filtering + const upDownMetric = scopeMetrics.metrics.find((m: any) => m.descriptor.name === 'test_updown'); + assert.ok(upDownMetric); + assert.deepStrictEqual(upDownMetric.dataPoints[0].attributes, { + region: 'us-west', + }); + }); +}); diff --git a/packages/sdk-metrics/test/view/ViewRegistry.test.ts b/packages/sdk-metrics/test/view/ViewRegistry.test.ts index bb1dadfbf01..194c0c04982 100644 --- a/packages/sdk-metrics/test/view/ViewRegistry.test.ts +++ b/packages/sdk-metrics/test/view/ViewRegistry.test.ts @@ -186,5 +186,136 @@ describe('ViewRegistry', () => { } }); }); + + describe('Advisory attributes parameter', () => { + it('should create default view with allow-list when no registered views match and instrument has advisory attributes', () => { + const registry = new ViewRegistry(); + + const instrumentWithAdvisoryAttributes = { + ...defaultInstrumentDescriptor, + name: 'test_instrument', + advice: { + attributes: ['key1', 'key2'] + } + }; + + const views = registry.findViews( + instrumentWithAdvisoryAttributes, + defaultInstrumentationScope + ); + + assert.strictEqual(views.length, 1); + assert.strictEqual(views[0].name, undefined); // Default view has no custom name + + // Test that the attributesProcessor is configured correctly + const processor = views[0].attributesProcessor; + const result = processor.process({ + key1: 'value1', + key2: 'value2', + key3: 'value3' // This should be filtered out + }); + + assert.deepStrictEqual(result, { + key1: 'value1', + key2: 'value2' + }); + }); + + it('should create default view without attribute filtering when instrument has no advisory attributes', () => { + const registry = new ViewRegistry(); + + const instrumentWithoutAdvisoryAttributes = { + ...defaultInstrumentDescriptor, + name: 'test_instrument_no_attributes', + advice: {} + }; + + const views = registry.findViews( + instrumentWithoutAdvisoryAttributes, + defaultInstrumentationScope + ); + + assert.strictEqual(views.length, 1); + + // Test that the attributesProcessor allows all attributes + const processor = views[0].attributesProcessor; + const inputAttrs = { + key1: 'value1', + key2: 'value2', + key3: 'value3' + }; + const result = processor.process(inputAttrs); + + assert.deepStrictEqual(result, inputAttrs); + }); + + it('should create default view without attribute filtering when instrument has empty advisory attributes', () => { + const registry = new ViewRegistry(); + + const instrumentWithEmptyAdvisoryAttributes = { + ...defaultInstrumentDescriptor, + name: 'test_instrument_empty_attributes', + advice: { + attributes: [] + } + }; + + const views = registry.findViews( + instrumentWithEmptyAdvisoryAttributes, + defaultInstrumentationScope + ); + + assert.strictEqual(views.length, 1); + + // Test that the attributesProcessor allows all attributes + const processor = views[0].attributesProcessor; + const inputAttrs = { + key1: 'value1', + key2: 'value2', + key3: 'value3' + }; + const result = processor.process(inputAttrs); + + assert.deepStrictEqual(result, inputAttrs); + }); + + it('should use registered views instead of advisory attributes when available', () => { + const registry = new ViewRegistry(); + + // Add a registered view + registry.addView(new View({ + name: 'custom_view', + instrumentName: 'test_instrument' + })); + + const instrumentWithAdvisoryAttributes = { + ...defaultInstrumentDescriptor, + name: 'test_instrument', + advice: { + attributes: ['key1', 'key2'] + } + }; + + const views = registry.findViews( + instrumentWithAdvisoryAttributes, + defaultInstrumentationScope + ); + + assert.strictEqual(views.length, 1); + assert.strictEqual(views[0].name, 'custom_view'); + + // The registered view should be used instead of advisory attributes + const processor = views[0].attributesProcessor; + const inputAttrs = { + key1: 'value1', + key2: 'value2', + key3: 'value3' + }; + const result = processor.process(inputAttrs); + + // Should allow all attributes since registered view doesn't have attribute filtering + assert.deepStrictEqual(result, inputAttrs); + }); + }); }); }); From d7c17af3cb452fb3bacc89178479a0567e18b919 Mon Sep 17 00:00:00 2001 From: Pratyush <116508117+pratstick@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:14:26 +0530 Subject: [PATCH 4/6] docs(metrics): document advisory attributes parameter with example Add documentation and example for the new advisory attributes parameter: - Update README with usage instructions and examples - Create standalone example demonstrating the feature - Include notes about experimental status and performance benefits - Provide clear guidance on when and how to use advisory attributes --- packages/sdk-metrics/README.md | 23 ++++++ .../examples/advisory-attributes.js | 81 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 packages/sdk-metrics/examples/advisory-attributes.js diff --git a/packages/sdk-metrics/README.md b/packages/sdk-metrics/README.md index 035a1de01b2..f5580ccc588 100644 --- a/packages/sdk-metrics/README.md +++ b/packages/sdk-metrics/README.md @@ -36,6 +36,29 @@ const counter = opentelemetry.metrics.getMeter('default').createCounter('foo'); counter.add(1, { attributeKey: 'attribute-value' }); ``` +### Advisory Attributes Parameter (Experimental) + +The Metrics API supports an optional `attributes` advisory parameter in the `advice` configuration when creating instruments. This parameter acts as an allow-list for attribute keys, filtering out all attributes that are not in the specified list: + +```js +const counter = opentelemetry.metrics.getMeter('default').createCounter('filtered-counter', { + description: 'A counter with attribute filtering', + advice: { + attributes: ['service', 'version'], // @experimental: Only these keys will be kept + }, +}); + +// Only 'service' and 'version' attributes will be recorded, others are filtered out +counter.add(1, { + service: 'api-gateway', + version: '1.0.0', + region: 'us-west', // This will be filtered out + method: 'GET' // This will be filtered out +}); +``` + +**Note:** This feature is experimental and may change in future versions. The advisory attributes parameter only applies when no Views are configured for the instrument. If Views are present, they take precedence over the instrument's advisory attributes. + In conditions, we may need to setup an async instrument to observe costly events: ```js diff --git a/packages/sdk-metrics/examples/advisory-attributes.js b/packages/sdk-metrics/examples/advisory-attributes.js new file mode 100644 index 00000000000..3b1d7484a6c --- /dev/null +++ b/packages/sdk-metrics/examples/advisory-attributes.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +/* + * This example demonstrates the new advisory attributes parameter for metrics instruments. + * The attributes parameter acts as an allow-list for the instrument, filtering out + * all attribute keys that are not in the specified list. + */ + +const { MeterProvider } = require('@opentelemetry/sdk-metrics'); +const { ConsoleMetricExporter } = require('@opentelemetry/sdk-metrics'); +const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); + +// Create a meter provider with a console exporter +const meterProvider = new MeterProvider({ + readers: [ + new PeriodicExportingMetricReader({ + exporter: new ConsoleMetricExporter(), + exportIntervalMillis: 1000, + }), + ], +}); + +const meter = meterProvider.getMeter('advisory-attributes-example'); + +// Create a counter with advisory attributes - only 'service' and 'version' keys will be kept +const requestCounter = meter.createCounter('http_requests_total', { + description: 'Total number of HTTP requests', + advice: { + attributes: ['service', 'version'], // @experimental: Only these keys will be allowed + }, +}); + +// Create a histogram without advisory attributes - all keys will be kept +const responseTimeHistogram = meter.createHistogram('http_response_time', { + description: 'HTTP response time in milliseconds', +}); + +// Record some measurements +console.log('Recording metrics with advisory attributes filtering...\n'); + +// This will only keep 'service' and 'version' attributes, filtering out 'method' and 'endpoint' +requestCounter.add(1, { + service: 'api-gateway', + version: '1.2.3', + method: 'GET', // This will be filtered out + endpoint: '/users', // This will be filtered out +}); + +requestCounter.add(1, { + service: 'user-service', + version: '2.1.0', + method: 'POST', // This will be filtered out + endpoint: '/auth', // This will be filtered out + region: 'us-west-2', // This will be filtered out +}); + +// This will keep all attributes since no advisory attributes are specified +responseTimeHistogram.record(150, { + service: 'api-gateway', + method: 'GET', + endpoint: '/users', + status_code: '200', +}); + +responseTimeHistogram.record(89, { + service: 'user-service', + method: 'POST', + endpoint: '/auth', + status_code: '201', + region: 'us-west-2', +}); + +console.log('Check the console output above to see the filtered attributes!'); +console.log('The http_requests_total metric should only have service and version attributes.'); +console.log('The http_response_time metric should have all attributes.'); + +// Keep the process running for a bit to see the output +setTimeout(() => { + console.log('\nShutting down...'); + meterProvider.shutdown(); +}, 2000); From 957e0bfeca33e419c8c3a8d187d49535364baf87 Mon Sep 17 00:00:00 2001 From: Pratyush <116508117+pratstick@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:21:07 +0530 Subject: [PATCH 5/6] Update api/src/metrics/Metric.ts Added the suggested change Co-authored-by: Marc Pichler --- api/src/metrics/Metric.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/src/metrics/Metric.ts b/api/src/metrics/Metric.ts index bee8aceeb42..839343b0779 100644 --- a/api/src/metrics/Metric.ts +++ b/api/src/metrics/Metric.ts @@ -35,8 +35,6 @@ export interface MetricAdvice { * @experimental * An allow-list of attribute keys for this instrument. Only these keys will be kept * on measurements recorded by this instrument. If not provided, all attributes are kept. - * - * See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument-advisory-parameter-attributes */ attributes?: string[]; } From b5eca96bd586d16c529b82f3afc63d939af04dd0 Mon Sep 17 00:00:00 2001 From: pratstick <116508117+pratstick@users.noreply.github.com> Date: Wed, 9 Jul 2025 01:17:48 +0530 Subject: [PATCH 6/6] feat(metrics): add advisory attributes parameter to metric instruments - Add experimental `attributes` parameter to MetricAdvice interface - Implement attribute filtering via ViewRegistry default views - Only applies when no Views are configured for the instrument - Views take precedence over advisory attributes when present - Add comprehensive unit and integration tests - Update documentation and changelogs Closes #4365 --- CHANGELOG.md | 5 ++ api/CHANGELOG.md | 4 + api/src/metrics/Metric.ts | 5 ++ packages/sdk-metrics/README.md | 46 +++++------ .../examples/advisory-attributes.js | 81 ------------------- packages/sdk-metrics/src/view/ViewRegistry.ts | 35 +++++--- 6 files changed, 59 insertions(+), 117 deletions(-) delete mode 100644 packages/sdk-metrics/examples/advisory-attributes.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8906fde42ce..fe251cabff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(metrics): add advisory attributes parameter to metric instruments [#4365](https://github.com/open-telemetry/opentelemetry-js/issues/4365) + * Added experimental `attributes` parameter to `MetricAdvice` for filtering measurement attributes + * Only applies when no Views are configured for the instrument + * Views take precedence over advisory attributes when present + ### :bug: Bug Fixes * fix(sdk-metrics): Remove invalid default value for `startTime` param to ExponentialHistogramAccumulation. This only impacted the closurescript compiler. [#5763](https://github.com/open-telemetry/opentelemetry-js/pull/5763) @trentm diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index ff8842f2a71..022261dbd8c 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. ### :rocket: (Enhancement) +* feat(metrics): add advisory attributes parameter to metric instruments [#4365](https://github.com/open-telemetry/opentelemetry-js/issues/4365) + * Added experimental `attributes` parameter to `MetricAdvice` interface + * Allows specifying an allow-list of attribute keys for metric instruments + * feat(api): improve isValidSpanId, isValidTraceId performance [#5714](https://github.com/open-telemetry/opentelemetry-js/pull/5714) @seemk * feat(diag): change types in `DiagComponentLogger` from `any` to `unknown`[#5478](https://github.com/open-telemetry/opentelemetry-js/pull/5478) @loganrosen diff --git a/api/src/metrics/Metric.ts b/api/src/metrics/Metric.ts index 839343b0779..6692d787549 100644 --- a/api/src/metrics/Metric.ts +++ b/api/src/metrics/Metric.ts @@ -35,6 +35,11 @@ export interface MetricAdvice { * @experimental * An allow-list of attribute keys for this instrument. Only these keys will be kept * on measurements recorded by this instrument. If not provided, all attributes are kept. + * + * @example only keep 'service' and 'version' attributes + * attributes: ['service', 'version'] + * @example keep all attributes (default behavior) + * attributes: undefined */ attributes?: string[]; } diff --git a/packages/sdk-metrics/README.md b/packages/sdk-metrics/README.md index f5580ccc588..705db572cf6 100644 --- a/packages/sdk-metrics/README.md +++ b/packages/sdk-metrics/README.md @@ -36,29 +36,6 @@ const counter = opentelemetry.metrics.getMeter('default').createCounter('foo'); counter.add(1, { attributeKey: 'attribute-value' }); ``` -### Advisory Attributes Parameter (Experimental) - -The Metrics API supports an optional `attributes` advisory parameter in the `advice` configuration when creating instruments. This parameter acts as an allow-list for attribute keys, filtering out all attributes that are not in the specified list: - -```js -const counter = opentelemetry.metrics.getMeter('default').createCounter('filtered-counter', { - description: 'A counter with attribute filtering', - advice: { - attributes: ['service', 'version'], // @experimental: Only these keys will be kept - }, -}); - -// Only 'service' and 'version' attributes will be recorded, others are filtered out -counter.add(1, { - service: 'api-gateway', - version: '1.0.0', - region: 'us-west', // This will be filtered out - method: 'GET' // This will be filtered out -}); -``` - -**Note:** This feature is experimental and may change in future versions. The advisory attributes parameter only applies when no Views are configured for the instrument. If Views are present, they take precedence over the instrument's advisory attributes. - In conditions, we may need to setup an async instrument to observe costly events: ```js @@ -102,6 +79,29 @@ const meterProvider = new MeterProvider({ }) ``` +## Advisory Attributes (Experimental) + +The Metrics API supports an optional `attributes` advisory parameter in the `advice` configuration when creating instruments. This parameter acts as an allow-list for attribute keys, filtering out all attributes that are not in the specified list: + +```js +const counter = opentelemetry.metrics.getMeter('default').createCounter('filtered-counter', { + description: 'A counter with attribute filtering', + advice: { + attributes: ['service', 'version'], // @experimental: Only these keys will be kept + }, +}); + +// Only 'service' and 'version' attributes will be recorded, others are filtered out +counter.add(1, { + service: 'api-gateway', + version: '1.0.0', + region: 'us-west', // This will be filtered out + method: 'GET' // This will be filtered out +}); +``` + +**Note:** This feature is experimental and may change in future versions. The advisory attributes parameter only applies when no Views are configured for the instrument. If Views are present, they take precedence over the instrument's advisory attributes. + ## Example See [examples/prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/examples/prometheus) for an end-to-end example, including exporting metrics. diff --git a/packages/sdk-metrics/examples/advisory-attributes.js b/packages/sdk-metrics/examples/advisory-attributes.js deleted file mode 100644 index 3b1d7484a6c..00000000000 --- a/packages/sdk-metrics/examples/advisory-attributes.js +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env node - -/* - * This example demonstrates the new advisory attributes parameter for metrics instruments. - * The attributes parameter acts as an allow-list for the instrument, filtering out - * all attribute keys that are not in the specified list. - */ - -const { MeterProvider } = require('@opentelemetry/sdk-metrics'); -const { ConsoleMetricExporter } = require('@opentelemetry/sdk-metrics'); -const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); - -// Create a meter provider with a console exporter -const meterProvider = new MeterProvider({ - readers: [ - new PeriodicExportingMetricReader({ - exporter: new ConsoleMetricExporter(), - exportIntervalMillis: 1000, - }), - ], -}); - -const meter = meterProvider.getMeter('advisory-attributes-example'); - -// Create a counter with advisory attributes - only 'service' and 'version' keys will be kept -const requestCounter = meter.createCounter('http_requests_total', { - description: 'Total number of HTTP requests', - advice: { - attributes: ['service', 'version'], // @experimental: Only these keys will be allowed - }, -}); - -// Create a histogram without advisory attributes - all keys will be kept -const responseTimeHistogram = meter.createHistogram('http_response_time', { - description: 'HTTP response time in milliseconds', -}); - -// Record some measurements -console.log('Recording metrics with advisory attributes filtering...\n'); - -// This will only keep 'service' and 'version' attributes, filtering out 'method' and 'endpoint' -requestCounter.add(1, { - service: 'api-gateway', - version: '1.2.3', - method: 'GET', // This will be filtered out - endpoint: '/users', // This will be filtered out -}); - -requestCounter.add(1, { - service: 'user-service', - version: '2.1.0', - method: 'POST', // This will be filtered out - endpoint: '/auth', // This will be filtered out - region: 'us-west-2', // This will be filtered out -}); - -// This will keep all attributes since no advisory attributes are specified -responseTimeHistogram.record(150, { - service: 'api-gateway', - method: 'GET', - endpoint: '/users', - status_code: '200', -}); - -responseTimeHistogram.record(89, { - service: 'user-service', - method: 'POST', - endpoint: '/auth', - status_code: '201', - region: 'us-west-2', -}); - -console.log('Check the console output above to see the filtered attributes!'); -console.log('The http_requests_total metric should only have service and version attributes.'); -console.log('The http_response_time metric should have all attributes.'); - -// Keep the process running for a bit to see the output -setTimeout(() => { - console.log('\nShutting down...'); - meterProvider.shutdown(); -}, 2000); diff --git a/packages/sdk-metrics/src/view/ViewRegistry.ts b/packages/sdk-metrics/src/view/ViewRegistry.ts index 9e0cf2c3198..5638549dd4f 100644 --- a/packages/sdk-metrics/src/view/ViewRegistry.ts +++ b/packages/sdk-metrics/src/view/ViewRegistry.ts @@ -18,7 +18,7 @@ import { InstrumentationScope } from '@opentelemetry/core'; import { InstrumentDescriptor } from '../InstrumentDescriptor'; import { InstrumentSelector } from './InstrumentSelector'; import { MeterSelector } from './MeterSelector'; -import { View } from './View'; +import { View, ViewOptions } from './View'; import { createAllowListAttributesProcessor } from './AttributesProcessor'; export class ViewRegistry { @@ -39,29 +39,38 @@ export class ViewRegistry { ); }); - // If no registered views match, create a default view with instrument's advisory attributes + // Only create a default view if advisory attributes are set and non-empty if (views.length === 0) { - const defaultView = this._createDefaultView(instrument); - return [defaultView]; + if ( + instrument.advice && + Array.isArray(instrument.advice.attributes) && + instrument.advice.attributes.length > 0 + ) { + return [this._createDefaultView(instrument)]; + } + // No matching views and no advisory attributes: return empty array for backward compatibility + return []; } return views; } private _createDefaultView(instrument: InstrumentDescriptor): View { - // Create default view with instrument's advisory attributes as an allow-list - const viewOptions: any = { + const viewOptions: ViewOptions = { instrumentName: instrument.name, instrumentType: instrument.type, instrumentUnit: instrument.unit, }; - - // If instrument has advisory attributes, use them as an allow-list - if (instrument.advice.attributes && instrument.advice.attributes.length > 0) { - viewOptions.attributesProcessors = [createAllowListAttributesProcessor(instrument.advice.attributes)]; + + if ( + instrument.advice.attributes && + instrument.advice.attributes.length > 0 + ) { + viewOptions.attributesProcessors = [ + createAllowListAttributesProcessor(instrument.advice.attributes), + ]; } - - // Create a view that matches this specific instrument + return new View(viewOptions); } @@ -89,4 +98,4 @@ export class ViewRegistry { selector.getSchemaUrlFilter().match(meter.schemaUrl)) ); } -} +} \ No newline at end of file