From 52d2f6652a871a328b1643a208cd16d6404f600d Mon Sep 17 00:00:00 2001 From: Shivanth MP Date: Tue, 5 Aug 2025 18:57:04 +0200 Subject: [PATCH] Add support for native histogram exemplars (#6772) Added support for exemplars in exponential histograms. Closes #5777 --------- Co-authored-by: Tyler Yahn --- CHANGELOG.md | 1 + exporters/prometheus/exporter.go | 3 +-- exporters/prometheus/exporter_test.go | 36 ++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c921778d..5cc352f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032) - Add experimental self-observability span metrics in `go.opentelemetry.io/otel/sdk/trace`. Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027) +- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772) ### Changed diff --git a/exporters/prometheus/exporter.go b/exporters/prometheus/exporter.go index 9664b3da8..f00c1ebf4 100644 --- a/exporters/prometheus/exporter.go +++ b/exporters/prometheus/exporter.go @@ -382,8 +382,7 @@ func addExponentialHistogramMetric[N int64 | float64]( otel.Handle(err) continue } - - // TODO(GiedriusS): add exemplars here after https://github.com/prometheus/client_golang/pull/1654#pullrequestreview-2434669425 is done. + m = addExemplars(m, dp.Exemplars, labelNamer) ch <- m } } diff --git a/exporters/prometheus/exporter_test.go b/exporters/prometheus/exporter_test.go index dfe9c45ea..ca2cee5d1 100644 --- a/exporters/prometheus/exporter_test.go +++ b/exporters/prometheus/exporter_test.go @@ -1113,6 +1113,18 @@ func TestExemplars(t *testing.T) { escapingScheme: model.NoEscaping, validationScheme: model.UTF8Validation, }, + { + name: "exponential histogram", + recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { + hist, err := meter.Int64Histogram("exponential_histogram") + require.NoError(t, err) + hist.Record(ctx, 9, attrsOpt) + }, + expectedExemplarValue: 9, + expectedLabels: expectedNonEscapedLabels, + escapingScheme: model.NoEscaping, + validationScheme: model.UTF8Validation, + }, } { t.Run(tc.name, func(t *testing.T) { originalEscapingScheme := model.NameEscapingScheme @@ -1144,13 +1156,24 @@ func TestExemplars(t *testing.T) { metric.WithReader(exporter), metric.WithResource(res), metric.WithView(metric.NewView( - metric.Instrument{Name: "*"}, + metric.Instrument{Name: "foo"}, metric.Stream{ // filter out all attributes so they are added as filtered // attributes to the exemplar AttributeFilter: attribute.NewAllowKeysFilter(), }, - )), + ), + ), + metric.WithView(metric.NewView( + metric.Instrument{Name: "exponential_histogram"}, + metric.Stream{ + Aggregation: metric.AggregationBase2ExponentialHistogram{ + MaxSize: 20, + }, + AttributeFilter: attribute.NewAllowKeysFilter(), + }, + ), + ), ) meter := provider.Meter("meter", otelmetric.WithInstrumentationVersion("v0.1.0")) @@ -1179,16 +1202,23 @@ func TestExemplars(t *testing.T) { case dto.MetricType_COUNTER: exemplar = metric.GetCounter().GetExemplar() case dto.MetricType_HISTOGRAM: - for _, b := range metric.GetHistogram().GetBucket() { + h := metric.GetHistogram() + for _, b := range h.GetBucket() { if b.GetExemplar() != nil { exemplar = b.GetExemplar() continue } } + if h.GetZeroThreshold() != 0 || h.GetZeroCount() != 0 || + len(h.PositiveSpan) != 0 || len(h.NegativeSpan) != 0 { + require.NotNil(t, h.Exemplars) + exemplar = h.Exemplars[0] + } } require.NotNil(t, exemplar) require.Equal(t, tc.expectedExemplarValue, exemplar.GetValue()) require.Len(t, exemplar.GetLabel(), len(tc.expectedLabels)) + for _, label := range exemplar.GetLabel() { val, ok := tc.expectedLabels[label.GetName()] require.True(t, ok)