Skip to content

Commit 82948e1

Browse files
committed
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
1 parent 09c03bd commit 82948e1

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { MeterProvider } from '../src/MeterProvider';
19+
import { TestMetricReader } from './export/TestMetricReader';
20+
import {
21+
defaultInstrumentationScope,
22+
testResource,
23+
} from './util';
24+
25+
describe('Advisory attributes parameter - Integration test', () => {
26+
let meterProvider: MeterProvider;
27+
let metricReader: TestMetricReader;
28+
29+
beforeEach(() => {
30+
metricReader = new TestMetricReader();
31+
meterProvider = new MeterProvider({
32+
resource: testResource,
33+
readers: [metricReader],
34+
});
35+
});
36+
37+
afterEach(() => {
38+
metricReader.shutdown();
39+
});
40+
41+
it('should filter attributes based on advisory attributes parameter', async () => {
42+
const meter = meterProvider.getMeter(
43+
defaultInstrumentationScope.name,
44+
defaultInstrumentationScope.version,
45+
{
46+
schemaUrl: defaultInstrumentationScope.schemaUrl,
47+
}
48+
);
49+
50+
// Create counter with advisory attributes
51+
const counter = meter.createCounter('test_counter', {
52+
description: 'Test counter with advisory attributes',
53+
advice: {
54+
attributes: ['allowed_key1', 'allowed_key2'], // @experimental
55+
},
56+
});
57+
58+
// Record measurements with various attributes
59+
counter.add(1, {
60+
allowed_key1: 'value1',
61+
allowed_key2: 'value2',
62+
filtered_key: 'filtered_value', // This should be filtered out
63+
});
64+
65+
counter.add(2, {
66+
allowed_key1: 'value3',
67+
another_filtered_key: 'another_filtered_value', // This should be filtered out
68+
});
69+
70+
// Collect metrics
71+
const metrics = await metricReader.collect();
72+
73+
assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1);
74+
75+
const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0];
76+
assert.strictEqual(scopeMetrics.metrics.length, 1);
77+
78+
const metric = scopeMetrics.metrics[0];
79+
assert.strictEqual(metric.descriptor.name, 'test_counter');
80+
assert.strictEqual(metric.dataPoints.length, 2);
81+
82+
// Verify that only allowed attributes are present
83+
const firstDataPoint = metric.dataPoints[0];
84+
assert.deepStrictEqual(firstDataPoint.attributes, {
85+
allowed_key1: 'value1',
86+
allowed_key2: 'value2',
87+
});
88+
89+
const secondDataPoint = metric.dataPoints[1];
90+
assert.deepStrictEqual(secondDataPoint.attributes, {
91+
allowed_key1: 'value3',
92+
});
93+
});
94+
95+
it('should keep all attributes when no advisory attributes are specified', async () => {
96+
const meter = meterProvider.getMeter(
97+
defaultInstrumentationScope.name,
98+
defaultInstrumentationScope.version,
99+
{
100+
schemaUrl: defaultInstrumentationScope.schemaUrl,
101+
}
102+
);
103+
104+
// Create counter without advisory attributes
105+
const counter = meter.createCounter('test_counter_no_filter', {
106+
description: 'Test counter without advisory attributes',
107+
});
108+
109+
// Record measurements with various attributes
110+
counter.add(1, {
111+
key1: 'value1',
112+
key2: 'value2',
113+
key3: 'value3',
114+
});
115+
116+
// Collect metrics
117+
const metrics = await metricReader.collect();
118+
119+
assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1);
120+
121+
const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0];
122+
assert.strictEqual(scopeMetrics.metrics.length, 1);
123+
124+
const metric = scopeMetrics.metrics[0];
125+
assert.strictEqual(metric.descriptor.name, 'test_counter_no_filter');
126+
assert.strictEqual(metric.dataPoints.length, 1);
127+
128+
// Verify that all attributes are present
129+
const dataPoint = metric.dataPoints[0];
130+
assert.deepStrictEqual(dataPoint.attributes, {
131+
key1: 'value1',
132+
key2: 'value2',
133+
key3: 'value3',
134+
});
135+
});
136+
137+
it('should work with different instrument types', async () => {
138+
const meter = meterProvider.getMeter(
139+
defaultInstrumentationScope.name,
140+
defaultInstrumentationScope.version,
141+
{
142+
schemaUrl: defaultInstrumentationScope.schemaUrl,
143+
}
144+
);
145+
146+
// Test with Histogram
147+
const histogram = meter.createHistogram('test_histogram', {
148+
description: 'Test histogram with advisory attributes',
149+
advice: {
150+
attributes: ['service'], // @experimental
151+
},
152+
});
153+
154+
histogram.record(10, {
155+
service: 'api',
156+
endpoint: '/users', // This should be filtered out
157+
});
158+
159+
// Test with UpDownCounter
160+
const upDownCounter = meter.createUpDownCounter('test_updown', {
161+
description: 'Test updown counter with advisory attributes',
162+
advice: {
163+
attributes: ['region'], // @experimental
164+
},
165+
});
166+
167+
upDownCounter.add(5, {
168+
region: 'us-west',
169+
instance: 'i-12345', // This should be filtered out
170+
});
171+
172+
// Collect metrics
173+
const metrics = await metricReader.collect();
174+
175+
assert.strictEqual(metrics.resourceMetrics.scopeMetrics.length, 1);
176+
177+
const scopeMetrics = metrics.resourceMetrics.scopeMetrics[0];
178+
assert.strictEqual(scopeMetrics.metrics.length, 2);
179+
180+
// Verify histogram filtering
181+
const histogramMetric = scopeMetrics.metrics.find((m: any) => m.descriptor.name === 'test_histogram');
182+
assert.ok(histogramMetric);
183+
assert.deepStrictEqual(histogramMetric.dataPoints[0].attributes, {
184+
service: 'api',
185+
});
186+
187+
// Verify updown counter filtering
188+
const upDownMetric = scopeMetrics.metrics.find((m: any) => m.descriptor.name === 'test_updown');
189+
assert.ok(upDownMetric);
190+
assert.deepStrictEqual(upDownMetric.dataPoints[0].attributes, {
191+
region: 'us-west',
192+
});
193+
});
194+
});

packages/sdk-metrics/test/view/ViewRegistry.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,5 +186,136 @@ describe('ViewRegistry', () => {
186186
}
187187
});
188188
});
189+
190+
describe('Advisory attributes parameter', () => {
191+
it('should create default view with allow-list when no registered views match and instrument has advisory attributes', () => {
192+
const registry = new ViewRegistry();
193+
194+
const instrumentWithAdvisoryAttributes = {
195+
...defaultInstrumentDescriptor,
196+
name: 'test_instrument',
197+
advice: {
198+
attributes: ['key1', 'key2']
199+
}
200+
};
201+
202+
const views = registry.findViews(
203+
instrumentWithAdvisoryAttributes,
204+
defaultInstrumentationScope
205+
);
206+
207+
assert.strictEqual(views.length, 1);
208+
assert.strictEqual(views[0].name, undefined); // Default view has no custom name
209+
210+
// Test that the attributesProcessor is configured correctly
211+
const processor = views[0].attributesProcessor;
212+
const result = processor.process({
213+
key1: 'value1',
214+
key2: 'value2',
215+
key3: 'value3' // This should be filtered out
216+
});
217+
218+
assert.deepStrictEqual(result, {
219+
key1: 'value1',
220+
key2: 'value2'
221+
});
222+
});
223+
224+
it('should create default view without attribute filtering when instrument has no advisory attributes', () => {
225+
const registry = new ViewRegistry();
226+
227+
const instrumentWithoutAdvisoryAttributes = {
228+
...defaultInstrumentDescriptor,
229+
name: 'test_instrument_no_attributes',
230+
advice: {}
231+
};
232+
233+
const views = registry.findViews(
234+
instrumentWithoutAdvisoryAttributes,
235+
defaultInstrumentationScope
236+
);
237+
238+
assert.strictEqual(views.length, 1);
239+
240+
// Test that the attributesProcessor allows all attributes
241+
const processor = views[0].attributesProcessor;
242+
const inputAttrs = {
243+
key1: 'value1',
244+
key2: 'value2',
245+
key3: 'value3'
246+
};
247+
const result = processor.process(inputAttrs);
248+
249+
assert.deepStrictEqual(result, inputAttrs);
250+
});
251+
252+
it('should create default view without attribute filtering when instrument has empty advisory attributes', () => {
253+
const registry = new ViewRegistry();
254+
255+
const instrumentWithEmptyAdvisoryAttributes = {
256+
...defaultInstrumentDescriptor,
257+
name: 'test_instrument_empty_attributes',
258+
advice: {
259+
attributes: []
260+
}
261+
};
262+
263+
const views = registry.findViews(
264+
instrumentWithEmptyAdvisoryAttributes,
265+
defaultInstrumentationScope
266+
);
267+
268+
assert.strictEqual(views.length, 1);
269+
270+
// Test that the attributesProcessor allows all attributes
271+
const processor = views[0].attributesProcessor;
272+
const inputAttrs = {
273+
key1: 'value1',
274+
key2: 'value2',
275+
key3: 'value3'
276+
};
277+
const result = processor.process(inputAttrs);
278+
279+
assert.deepStrictEqual(result, inputAttrs);
280+
});
281+
282+
it('should use registered views instead of advisory attributes when available', () => {
283+
const registry = new ViewRegistry();
284+
285+
// Add a registered view
286+
registry.addView(new View({
287+
name: 'custom_view',
288+
instrumentName: 'test_instrument'
289+
}));
290+
291+
const instrumentWithAdvisoryAttributes = {
292+
...defaultInstrumentDescriptor,
293+
name: 'test_instrument',
294+
advice: {
295+
attributes: ['key1', 'key2']
296+
}
297+
};
298+
299+
const views = registry.findViews(
300+
instrumentWithAdvisoryAttributes,
301+
defaultInstrumentationScope
302+
);
303+
304+
assert.strictEqual(views.length, 1);
305+
assert.strictEqual(views[0].name, 'custom_view');
306+
307+
// The registered view should be used instead of advisory attributes
308+
const processor = views[0].attributesProcessor;
309+
const inputAttrs = {
310+
key1: 'value1',
311+
key2: 'value2',
312+
key3: 'value3'
313+
};
314+
const result = processor.process(inputAttrs);
315+
316+
// Should allow all attributes since registered view doesn't have attribute filtering
317+
assert.deepStrictEqual(result, inputAttrs);
318+
});
319+
});
189320
});
190321
});

0 commit comments

Comments
 (0)