Skip to content

feat(sdk-metrics): wire exemplar support into metrics pipeline#6483

Open
CharlieTLe wants to merge 1 commit intoopen-telemetry:mainfrom
CharlieTLe:feat/metrics-exemplars
Open

feat(sdk-metrics): wire exemplar support into metrics pipeline#6483
CharlieTLe wants to merge 1 commit intoopen-telemetry:mainfrom
CharlieTLe:feat/metrics-exemplars

Conversation

@CharlieTLe
Copy link

Summary

Connects the existing exemplar infrastructure (filters, reservoirs) to the metrics SDK pipeline, enabling exemplar collection, storage, and export per the OpenTelemetry specification. Resolves #5147.

  • Add optional exemplars field to DataPoint<T> and export all exemplar types from the public API
  • Add exemplarFilter option to MeterProviderOptions with OTEL_METRICS_EXEMPLAR_FILTER env var support (always_on, always_off, trace_based)
  • Add exemplarReservoir factory option to View configuration for custom reservoir selection
  • Wire exemplar recording and collection into SyncMetricStorage with per-attribute-set reservoir management
  • Create default reservoir selection logic per spec: histogram → AlignedHistogramBucketExemplarReservoir, exponential histogram → SimpleFixedSizeExemplarReservoir(min(20, maxSize)), all others → SimpleFixedSizeExemplarReservoir(1)
  • Extend AccumulationRecord tuple to carry exemplars through the aggregation pipeline to all aggregator toMetricData() methods
  • Serialize exemplars in @opentelemetry/otlp-transformer for all data point types (number, histogram, exponential histogram)
  • Fix pre-existing mutation bug in ExemplarBucket.collect() that corrupted shared attributes objects via in-place delete

Test plan

  • 10 new integration tests covering all exemplar filters, custom reservoirs, filtered attributes, histogram exemplars, and env var configuration
  • All 403 existing sdk-metrics tests pass (zero regressions)
  • All 59 existing otlp-transformer tests pass (zero regressions)
  • Lint passes clean on both packages

🤖 Generated with Claude Code

@CharlieTLe CharlieTLe requested a review from a team as a code owner March 7, 2026 22:09
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Mar 7, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: CharlieTLe / name: Charlie Le (0e5c033)

@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.73%. Comparing base (356ddee) to head (0e5c033).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6483      +/-   ##
==========================================
+ Coverage   95.70%   95.73%   +0.03%     
==========================================
  Files         364      365       +1     
  Lines       11779    11870      +91     
  Branches     2751     2787      +36     
==========================================
+ Hits        11273    11364      +91     
  Misses        506      506              
Files with missing lines Coverage Δ
...ges/otlp-transformer/src/metrics/internal-types.ts 100.00% <ø> (ø)
.../packages/otlp-transformer/src/metrics/internal.ts 100.00% <100.00%> (ø)
packages/sdk-metrics/src/MeterProvider.ts 100.00% <100.00%> (ø)
...sdk-metrics/src/aggregator/ExponentialHistogram.ts 97.83% <100.00%> (+<0.01%) ⬆️
packages/sdk-metrics/src/aggregator/Histogram.ts 92.59% <100.00%> (+0.09%) ⬆️
packages/sdk-metrics/src/aggregator/LastValue.ts 100.00% <100.00%> (ø)
packages/sdk-metrics/src/aggregator/Sum.ts 100.00% <100.00%> (ø)
packages/sdk-metrics/src/aggregator/types.ts 100.00% <ø> (ø)
...ages/sdk-metrics/src/exemplar/ExemplarReservoir.ts 95.55% <100.00%> (ø)
...k-metrics/src/exemplar/ExemplarReservoirFactory.ts 100.00% <100.00%> (ø)
... and 7 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CharlieTLe CharlieTLe force-pushed the feat/metrics-exemplars branch 2 times, most recently from cbc8584 to 8de7c02 Compare March 8, 2026 00:21
Wire the existing exemplar infrastructure (filters, reservoirs) into
the metrics SDK so that exemplars are recorded, collected, and exported.

- Add exemplarFilter option to MeterProviderOptions
- Support OTEL_METRICS_EXEMPLAR_FILTER env var (always_on, always_off, trace_based)
- Add exemplarReservoir factory option to ViewOptions
- Record exemplars in SyncMetricStorage with per-attribute reservoirs
- Default reservoir selection per spec (aligned histogram buckets, fixed-size)
- Attach exemplars to DataPoint in all aggregators (Sum, Histogram, ExponentialHistogram, LastValue)
- Serialize exemplars in otlp-transformer for all data point types
- Fix mutation bug in ExemplarBucket.collect() that corrupted shared attributes
- Export exemplar types from sdk-metrics public API
- Add integration tests and exemplars demo (docker-compose with Grafana, Prometheus, Jaeger)
@CharlieTLe CharlieTLe force-pushed the feat/metrics-exemplars branch from 8e5a671 to 0e5c033 Compare March 8, 2026 21:24
Comment on lines 153 to +168
@@ -154,6 +161,11 @@ function toHistogramDataPoints(
startTimeUnixNano: encoder.encodeHrTime(dataPoint.startTime),
timeUnixNano: encoder.encodeHrTime(dataPoint.endTime),
};
const exemplars = toExemplars(dataPoint.exemplars, encoder);
if (exemplars) {
dp.exemplars = exemplars;
}
return dp;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are exemplars treated differently than other fields? If toExemplar returns undefined or [] it is the same as the field being dropped.

break;
}

const exemplars = toExemplars(dataPoint.exemplars, encoder);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exemplars can be included in the initial construction of out

};
}),
};
return exemplars?.length ? { ...dp, exemplars } : dp;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not include exemplars in dp always?

count: pointValue.count,
},
};
return exemplars?.length ? { ...dp, exemplars } : dp;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

endTime,
value: accumulation.toPointValue(),
};
return exemplars?.length ? { ...dp, exemplars } : dp;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

endTime,
value: accumulation.toPointValue(),
};
return exemplars?.length ? { ...dp, exemplars } : dp;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

return Array.from(map.entries()).map(
([attributes, accumulation, hashCode]) => {
const ex = exemplars.get(attributes, hashCode);
return ex && ex.length > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for ternary here. Array of length zero or undefined is valid for the return type

Comment on lines +67 to +75
export type { Exemplar } from './exemplar/Exemplar';
export type { ExemplarFilter } from './exemplar/ExemplarFilter';
export type { ExemplarReservoir } from './exemplar/ExemplarReservoir';
export { AlwaysSampleExemplarFilter } from './exemplar/AlwaysSampleExemplarFilter';
export { NeverSampleExemplarFilter } from './exemplar/NeverSampleExemplarFilter';
export { WithTraceExemplarFilter } from './exemplar/WithTraceExemplarFilter';
export { SimpleFixedSizeExemplarReservoir } from './exemplar/SimpleFixedSizeExemplarReservoir';
export { AlignedHistogramBucketExemplarReservoir } from './exemplar/AlignedHistogramBucketExemplarReservoir';
export { FixedSizeExemplarReservoirBase } from './exemplar/ExemplarReservoir';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to export all of these publicly? For example FixedSizeExemplarReservoirBase seems like an internal detail

Comment on lines +20 to +23
import type { ExemplarFilter } from './exemplar/ExemplarFilter';
import { WithTraceExemplarFilter } from './exemplar/WithTraceExemplarFilter';
import { AlwaysSampleExemplarFilter } from './exemplar/AlwaysSampleExemplarFilter';
import { NeverSampleExemplarFilter } from './exemplar/NeverSampleExemplarFilter';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

private _shutdown = false;

constructor(options?: MeterProviderOptions) {
const exemplarFilter =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to name a variable that's only used once

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sdk-metrics] implement metrics exemplars

2 participants