Skip to content

Commit 36ce569

Browse files
feat(sdk-metrics): adds the cardinalitySelector argument to PeriodicExportingMetricReaders (#6460)
Co-authored-by: Marylia Gutierrez <maryliag@gmail.com>
1 parent d397386 commit 36ce569

4 files changed

Lines changed: 257 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1010

1111
## Unreleased
1212

13+
* feat(sdk-metrics): adds the cardinalitySelector argument to PeriodicExportingMetricReaders
14+
[#6460](https://github.com/open-telemetry/opentelemetry-js/pull/6460) @starzlocker
15+
1316
### :boom: Breaking Changes
1417

1518
### :rocket: Features

packages/sdk-metrics/README.md

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,34 @@ opentelemetry.metrics.setGlobalMeterProvider(new MeterProvider());
3333
const counter = opentelemetry.metrics.getMeter('default').createCounter('foo');
3434

3535
// record a metric event.
36+
// NOTE: By default, each instrument can track up to 2000 unique time series.
37+
// This can be configured using cardinalityLimits. See "Configuring Cardinality Limits" below.
3638
counter.add(1, { attributeKey: 'attribute-value' });
3739
```
3840

3941
In conditions, we may need to setup an async instrument to observe costly events:
4042

4143
```js
4244
// Creating an async instrument, similar to synchronous instruments
43-
const observableCounter = opentelemetry.metrics.getMeter('default')
45+
const observableCounter = opentelemetry.metrics
46+
.getMeter('default')
4447
.createObservableCounter('observable-counter');
4548

4649
// Register a single-instrument callback to the async instrument.
47-
observableCounter.addCallback(async (observableResult) => {
50+
observableCounter.addCallback(async observableResult => {
4851
// ... do async stuff
4952
observableResult.observe(1, { attributeKey: 'attribute-value' });
5053
});
5154

5255
// Register a multi-instrument callback and associate it with a set of async instruments.
53-
opentelemetry.metrics.getMeter('default')
54-
.addBatchObservableCallback(batchObservableCallback, [ observableCounter ]);
56+
opentelemetry.metrics
57+
.getMeter('default')
58+
.addBatchObservableCallback(batchObservableCallback, [observableCounter]);
5559
async function batchObservableCallback(batchObservableResult) {
5660
// ... do async stuff
57-
batchObservableResult.observe(observableCounter, 1, { attributeKey: 'attribute-value' });
61+
batchObservableResult.observe(observableCounter, 1, {
62+
attributeKey: 'attribute-value',
63+
});
5864
}
5965
```
6066

@@ -68,15 +74,71 @@ const meterProvider = new MeterProvider({
6874
aggregation: {
6975
type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
7076
options: {
71-
boundaries: [0, 50, 100]
72-
}
77+
boundaries: [0, 50, 100],
78+
},
7379
},
74-
instrumentName: 'my.histogram'
80+
instrumentName: 'my.histogram',
7581
},
7682
// rename 'my.counter' to 'my.renamed.counter'
77-
{ name: 'my.renamed.counter', instrumentName: 'my.counter'}
78-
]
79-
})
83+
{ name: 'my.renamed.counter', instrumentName: 'my.counter' },
84+
],
85+
});
86+
```
87+
88+
## Configuring Cardinality Limits
89+
90+
The `cardinalityLimits` is an optional property in `PeriodicExportingMetricReader` that allows configuration of the maximum cardinality limits per instrument type (`InstrumentType`). This limit controls the maximum number of unique time series that can be tracked for each metric instrument. If not specified in the property, the limit will default to 2000 (the default value can also be specified).
91+
92+
It is converted to a `cardinalitySelector` function that:
93+
94+
- Takes an `InstrumentType` as input
95+
- Returns the configured cardinality limit for that instrument type
96+
- Falls back to the default value if a specific type isn't configured
97+
- Uses 2000 as the default if the default value is also not specified
98+
99+
If the `cardinalityLimits` property is omitted:
100+
101+
```js
102+
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
103+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
104+
105+
const exporter = new OTLPMetricExporter();
106+
const reader = new PeriodicExportingMetricReader({
107+
exporter,
108+
exportIntervalMillis: 60000,
109+
});
110+
111+
// All instruments will use the default limit of 2000 time series
112+
```
113+
114+
Configuring specific instrument types:
115+
116+
```js
117+
const reader = new PeriodicExportingMetricReader({
118+
exporter,
119+
exportIntervalMillis: 60000,
120+
cardinalityLimits: {
121+
counter: 10000, // Counters can have up to 10,000 time series
122+
histogram: 5000, // Histograms limited to 5,000 time series
123+
gauge: 3000, // Gauges limited to 3,000 time series
124+
default: 2500, // changes the default from 2000 to 2500
125+
},
126+
});
127+
```
128+
129+
Available configuration options:
130+
131+
```js
132+
type cardinalityLimits = {
133+
counter?: number;
134+
gauge?: number;
135+
histogram?: number;
136+
upDownCounter?: number;
137+
observableCounter?: number;
138+
observableGauge?: number;
139+
observableUpDownCounter?: number;
140+
default?: number;
141+
};
80142
```
81143

82144
## Example

packages/sdk-metrics/src/export/PeriodicExportingMetricReader.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MetricReader } from './MetricReader';
1313
import type { PushMetricExporter } from './MetricExporter';
1414
import { callWithTimeout, TimeoutError } from '../utils';
1515
import type { MetricProducer } from './MetricProducer';
16+
import { InstrumentType } from './MetricData';
1617

1718
export type PeriodicExportingMetricReaderOptions = {
1819
/**
@@ -35,6 +36,20 @@ export type PeriodicExportingMetricReaderOptions = {
3536
* @experimental
3637
*/
3738
metricProducers?: MetricProducer[];
39+
/**
40+
* Cardinality limits for the metric reader, applied per instrument. If not configured, defaults to 2000 time series per instrument. These are wrapped in a cardinalitySelector function that returns limits based on the instrument type, so they can be configured differently per type if desired.
41+
*
42+
*/
43+
cardinalityLimits?: {
44+
counter?: number;
45+
gauge?: number;
46+
histogram?: number;
47+
upDownCounter?: number;
48+
observableCounter?: number;
49+
observableGauge?: number;
50+
observableUpDownCounter?: number;
51+
default?: number;
52+
};
3853
};
3954

4055
/**
@@ -48,14 +63,44 @@ export class PeriodicExportingMetricReader extends MetricReader {
4863
private readonly _exportTimeout: number;
4964

5065
constructor(options: PeriodicExportingMetricReaderOptions) {
51-
const { exporter, exportIntervalMillis = 60000, metricProducers } = options;
66+
const {
67+
exporter,
68+
exportIntervalMillis = 60000,
69+
metricProducers,
70+
cardinalityLimits,
71+
} = options;
5272
let { exportTimeoutMillis = 30000 } = options;
5373

5474
super({
5575
aggregationSelector: exporter.selectAggregation?.bind(exporter),
5676
aggregationTemporalitySelector:
5777
exporter.selectAggregationTemporality?.bind(exporter),
5878
metricProducers,
79+
cardinalitySelector: (instrumentType: InstrumentType) => {
80+
const limits = {
81+
default: 2000,
82+
...cardinalityLimits,
83+
};
84+
85+
switch (instrumentType) {
86+
case InstrumentType.COUNTER:
87+
return limits.counter ?? limits.default;
88+
case InstrumentType.GAUGE:
89+
return limits.gauge ?? limits.default;
90+
case InstrumentType.HISTOGRAM:
91+
return limits.histogram ?? limits.default;
92+
case InstrumentType.OBSERVABLE_COUNTER:
93+
return limits.observableCounter ?? limits.default;
94+
case InstrumentType.OBSERVABLE_UP_DOWN_COUNTER:
95+
return limits.observableUpDownCounter ?? limits.default;
96+
case InstrumentType.OBSERVABLE_GAUGE:
97+
return limits.observableGauge ?? limits.default;
98+
case InstrumentType.UP_DOWN_COUNTER:
99+
return limits.upDownCounter ?? limits.default;
100+
default:
101+
return limits.default;
102+
}
103+
},
59104
});
60105

61106
if (exportIntervalMillis <= 0) {

packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { AggregationTemporality } from '../../src/export/AggregationTemporality'
88
import type {
99
AggregationOption,
1010
CollectionResult,
11-
InstrumentType,
1211
MetricProducer,
1312
PushMetricExporter,
1413
} from '../../src';
14+
import { InstrumentType } from '../../src/export/MetricData';
1515
import { AggregationType } from '../../src';
1616
import type {
1717
ResourceMetrics,
@@ -250,6 +250,140 @@ describe('PeriodicExportingMetricReader', () => {
250250
});
251251
});
252252

253+
describe('cardinalityLimits', () => {
254+
const instrumentTypes = [
255+
{ instrument: InstrumentType.COUNTER, name: 'counter' },
256+
{ instrument: InstrumentType.GAUGE, name: 'gauge' },
257+
{ instrument: InstrumentType.HISTOGRAM, name: 'histogram' },
258+
{ instrument: InstrumentType.UP_DOWN_COUNTER, name: 'upDownCounter' },
259+
{
260+
instrument: InstrumentType.OBSERVABLE_COUNTER,
261+
name: 'observableCounter',
262+
},
263+
{ instrument: InstrumentType.OBSERVABLE_GAUGE, name: 'observableGauge' },
264+
{
265+
instrument: InstrumentType.OBSERVABLE_UP_DOWN_COUNTER,
266+
name: 'observableUpDownCounter',
267+
},
268+
] as const;
269+
270+
it('should use default cardinality limit value if not specified', () => {
271+
const exporter = new TestDeltaMetricExporter();
272+
273+
const p = new PeriodicExportingMetricReader({
274+
exporter,
275+
exportIntervalMillis: 1,
276+
exportTimeoutMillis: 1,
277+
});
278+
279+
assert.strictEqual(
280+
p.selectCardinalityLimit(InstrumentType.COUNTER),
281+
2000
282+
);
283+
});
284+
285+
instrumentTypes.forEach(({ instrument, name }) => {
286+
it('should use the provided cardinality limit for ' + name, () => {
287+
const exporter = new TestDeltaMetricExporter();
288+
289+
const p = new PeriodicExportingMetricReader({
290+
exporter,
291+
exportIntervalMillis: 1,
292+
exportTimeoutMillis: 1,
293+
cardinalityLimits: {
294+
[name]: 5000,
295+
},
296+
});
297+
assert.strictEqual(p.selectCardinalityLimit(instrument), 5000);
298+
});
299+
});
300+
301+
it('should use the provided default cardinality limit when no specific limit is provided', () => {
302+
const exporter = new TestDeltaMetricExporter();
303+
304+
const defaultLimit = 1;
305+
306+
const instrumentTypesValues = [
307+
InstrumentType.COUNTER,
308+
InstrumentType.GAUGE,
309+
InstrumentType.HISTOGRAM,
310+
InstrumentType.UP_DOWN_COUNTER,
311+
InstrumentType.OBSERVABLE_COUNTER,
312+
InstrumentType.OBSERVABLE_GAUGE,
313+
InstrumentType.OBSERVABLE_UP_DOWN_COUNTER,
314+
];
315+
316+
const p = new PeriodicExportingMetricReader({
317+
exporter,
318+
exportIntervalMillis: 1,
319+
exportTimeoutMillis: 1,
320+
cardinalityLimits: { default: defaultLimit },
321+
});
322+
323+
instrumentTypesValues.forEach(instrument => {
324+
assert.strictEqual(p.selectCardinalityLimit(instrument), defaultLimit);
325+
});
326+
});
327+
328+
it('should use the provided values for a given instrument type, or fallback to the default value otherwise', () => {
329+
const exporter = new TestDeltaMetricExporter();
330+
331+
const p = new PeriodicExportingMetricReader({
332+
exporter,
333+
exportIntervalMillis: 1,
334+
exportTimeoutMillis: 1,
335+
cardinalityLimits: {
336+
counter: 1000,
337+
histogram: 3000,
338+
observableCounter: 4000,
339+
},
340+
});
341+
342+
assert.strictEqual(
343+
p.selectCardinalityLimit(InstrumentType.COUNTER),
344+
1000
345+
);
346+
assert.strictEqual(
347+
p.selectCardinalityLimit(InstrumentType.HISTOGRAM),
348+
3000
349+
);
350+
assert.strictEqual(
351+
p.selectCardinalityLimit(InstrumentType.OBSERVABLE_COUNTER),
352+
4000
353+
);
354+
assert.strictEqual(p.selectCardinalityLimit(InstrumentType.GAUGE), 2000);
355+
assert.strictEqual(
356+
p.selectCardinalityLimit(InstrumentType.UP_DOWN_COUNTER),
357+
2000
358+
);
359+
assert.strictEqual(
360+
p.selectCardinalityLimit(InstrumentType.OBSERVABLE_GAUGE),
361+
2000
362+
);
363+
assert.strictEqual(
364+
p.selectCardinalityLimit(InstrumentType.OBSERVABLE_UP_DOWN_COUNTER),
365+
2000
366+
);
367+
});
368+
369+
it("should fallback to the default value if the specified cardinality selector doesn't exists", () => {
370+
const exporter = new TestDeltaMetricExporter();
371+
const defaultLimit = 1;
372+
373+
const p = new PeriodicExportingMetricReader({
374+
exporter,
375+
exportIntervalMillis: 1,
376+
exportTimeoutMillis: 1,
377+
cardinalityLimits: { default: defaultLimit },
378+
});
379+
380+
assert.strictEqual(
381+
p.selectCardinalityLimit('' as InstrumentType),
382+
defaultLimit
383+
);
384+
});
385+
});
386+
253387
describe('setMetricProducer', () => {
254388
it('should start exporting periodically', async () => {
255389
const exporter = new TestMetricExporter();

0 commit comments

Comments
 (0)