Merge b5eca96bd5 into a7a36499f7
This commit is contained in:
commit
d36b9078a5
|
|
@ -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
|
||||
|
||||
### :books: Documentation
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@ 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.
|
||||
*
|
||||
* @example <caption>only keep 'service' and 'version' attributes</caption>
|
||||
* attributes: ['service', 'version']
|
||||
* @example <caption>keep all attributes (default behavior)</caption>
|
||||
* attributes: undefined
|
||||
*/
|
||||
attributes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -79,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.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ 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 {
|
||||
private _registeredViews: View[] = [];
|
||||
|
|
@ -38,9 +39,41 @@ export class ViewRegistry {
|
|||
);
|
||||
});
|
||||
|
||||
// Only create a default view if advisory attributes are set and non-empty
|
||||
if (views.length === 0) {
|
||||
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 {
|
||||
const viewOptions: ViewOptions = {
|
||||
instrumentName: instrument.name,
|
||||
instrumentType: instrument.type,
|
||||
instrumentUnit: instrument.unit,
|
||||
};
|
||||
|
||||
if (
|
||||
instrument.advice.attributes &&
|
||||
instrument.advice.attributes.length > 0
|
||||
) {
|
||||
viewOptions.attributesProcessors = [
|
||||
createAllowListAttributesProcessor(instrument.advice.attributes),
|
||||
];
|
||||
}
|
||||
|
||||
return new View(viewOptions);
|
||||
}
|
||||
|
||||
private _matchInstrument(
|
||||
selector: InstrumentSelector,
|
||||
instrument: InstrumentDescriptor
|
||||
|
|
@ -65,4 +98,4 @@ export class ViewRegistry {
|
|||
selector.getSchemaUrlFilter().match(meter.schemaUrl))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue