feat(sdk-metrics): wire exemplar support into metrics pipeline#6483
feat(sdk-metrics): wire exemplar support into metrics pipeline#6483CharlieTLe wants to merge 1 commit intoopen-telemetry:mainfrom
Conversation
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
cbc8584 to
8de7c02
Compare
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)
8e5a671 to
0e5c033
Compare
| @@ -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; | |||
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Exemplars can be included in the initial construction of out
| }; | ||
| }), | ||
| }; | ||
| return exemplars?.length ? { ...dp, exemplars } : dp; |
There was a problem hiding this comment.
Why not include exemplars in dp always?
| count: pointValue.count, | ||
| }, | ||
| }; | ||
| return exemplars?.length ? { ...dp, exemplars } : dp; |
| endTime, | ||
| value: accumulation.toPointValue(), | ||
| }; | ||
| return exemplars?.length ? { ...dp, exemplars } : dp; |
| endTime, | ||
| value: accumulation.toPointValue(), | ||
| }; | ||
| return exemplars?.length ? { ...dp, exemplars } : dp; |
| return Array.from(map.entries()).map( | ||
| ([attributes, accumulation, hashCode]) => { | ||
| const ex = exemplars.get(attributes, hashCode); | ||
| return ex && ex.length > 0 |
There was a problem hiding this comment.
No need for ternary here. Array of length zero or undefined is valid for the return type
| 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'; |
There was a problem hiding this comment.
Is it necessary to export all of these publicly? For example FixedSizeExemplarReservoirBase seems like an internal detail
| import type { ExemplarFilter } from './exemplar/ExemplarFilter'; | ||
| import { WithTraceExemplarFilter } from './exemplar/WithTraceExemplarFilter'; | ||
| import { AlwaysSampleExemplarFilter } from './exemplar/AlwaysSampleExemplarFilter'; | ||
| import { NeverSampleExemplarFilter } from './exemplar/NeverSampleExemplarFilter'; |
| private _shutdown = false; | ||
|
|
||
| constructor(options?: MeterProviderOptions) { | ||
| const exemplarFilter = |
There was a problem hiding this comment.
No need to name a variable that's only used once
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.
exemplarsfield toDataPoint<T>and export all exemplar types from the public APIexemplarFilteroption toMeterProviderOptionswithOTEL_METRICS_EXEMPLAR_FILTERenv var support (always_on,always_off,trace_based)exemplarReservoirfactory option toViewconfiguration for custom reservoir selectionSyncMetricStoragewith per-attribute-set reservoir managementAlignedHistogramBucketExemplarReservoir, exponential histogram →SimpleFixedSizeExemplarReservoir(min(20, maxSize)), all others →SimpleFixedSizeExemplarReservoir(1)AccumulationRecordtuple to carry exemplars through the aggregation pipeline to all aggregatortoMetricData()methods@opentelemetry/otlp-transformerfor all data point types (number, histogram, exponential histogram)ExemplarBucket.collect()that corrupted shared attributes objects via in-placedeleteTest plan
sdk-metricstests pass (zero regressions)otlp-transformertests pass (zero regressions)🤖 Generated with Claude Code