Skip to content

Commit 0168437

Browse files
authored
Add exemplar support to the prometheus exporter (#5111)
1 parent e6e4e4a commit 0168437

File tree

4 files changed

+159
-2
lines changed

4 files changed

+159
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2525
- Add `otel.scope.name` and `otel.scope.version` tags to spans exported by `go.opentelemetry.io/otel/exporters/zipkin`. (#5108)
2626
- Add support for `AddLink` to `go.opentelemetry.io/otel/bridge/opencensus`. (#5116)
2727
- Add `String` method to `Value` and `KeyValue` in `go.opentelemetry.io/otel/log`. (#5117)
28+
- Add Exemplar support to `go.opentelemetry.io/otel/exporters/prometheus`. (#5111)
2829

2930
### Changed
3031

exporters/prometheus/exporter.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package prometheus // import "go.opentelemetry.io/otel/exporters/prometheus"
55

66
import (
77
"context"
8+
"encoding/hex"
89
"errors"
910
"fmt"
1011
"slices"
@@ -32,6 +33,9 @@ const (
3233

3334
scopeInfoMetricName = "otel_scope_info"
3435
scopeInfoDescription = "Instrumentation Scope metadata"
36+
37+
traceIDExemplarKey = "trace_id"
38+
spanIDExemplarKey = "span_id"
3539
)
3640

3741
var (
@@ -238,7 +242,6 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
238242
}
239243

240244
func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogram metricdata.Histogram[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
241-
// TODO(https://github.com/open-telemetry/opentelemetry-go/issues/3163): support exemplars
242245
for _, dp := range histogram.DataPoints {
243246
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)
244247

@@ -255,6 +258,7 @@ func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogra
255258
otel.Handle(err)
256259
continue
257260
}
261+
m = addExemplars(m, dp.Exemplars)
258262
ch <- m
259263
}
260264
}
@@ -274,6 +278,7 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
274278
otel.Handle(err)
275279
continue
276280
}
281+
m = addExemplars(m, dp.Exemplars)
277282
ch <- m
278283
}
279284
}
@@ -549,3 +554,37 @@ func (c *collector) validateMetrics(name, description string, metricType *dto.Me
549554

550555
return false, ""
551556
}
557+
558+
func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata.Exemplar[N]) prometheus.Metric {
559+
if len(exemplars) == 0 {
560+
return m
561+
}
562+
promExemplars := make([]prometheus.Exemplar, len(exemplars))
563+
for i, exemplar := range exemplars {
564+
labels := attributesToLabels(exemplar.FilteredAttributes)
565+
// Overwrite any existing trace ID or span ID attributes
566+
labels[traceIDExemplarKey] = hex.EncodeToString(exemplar.TraceID[:])
567+
labels[spanIDExemplarKey] = hex.EncodeToString(exemplar.SpanID[:])
568+
promExemplars[i] = prometheus.Exemplar{
569+
Value: float64(exemplar.Value),
570+
Timestamp: exemplar.Time,
571+
Labels: labels,
572+
}
573+
}
574+
metricWithExemplar, err := prometheus.NewMetricWithExemplars(m, promExemplars...)
575+
if err != nil {
576+
// If there are errors creating the metric with exemplars, just warn
577+
// and return the metric without exemplars.
578+
otel.Handle(err)
579+
return m
580+
}
581+
return metricWithExemplar
582+
}
583+
584+
func attributesToLabels(attrs []attribute.KeyValue) prometheus.Labels {
585+
labels := make(map[string]string)
586+
for _, attr := range attrs {
587+
labels[string(attr.Key)] = attr.Value.Emit()
588+
}
589+
return labels
590+
}

exporters/prometheus/exporter_test.go

+117
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/prometheus/client_golang/prometheus"
1515
"github.com/prometheus/client_golang/prometheus/testutil"
16+
dto "github.com/prometheus/client_model/go"
1617
"github.com/stretchr/testify/assert"
1718
"github.com/stretchr/testify/require"
1819

@@ -22,6 +23,7 @@ import (
2223
"go.opentelemetry.io/otel/sdk/metric"
2324
"go.opentelemetry.io/otel/sdk/resource"
2425
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
26+
"go.opentelemetry.io/otel/trace"
2527
)
2628

2729
func TestPrometheusExporter(t *testing.T) {
@@ -898,3 +900,118 @@ func TestShutdownExporter(t *testing.T) {
898900
// ensure we aren't unnecessarily logging errors from the shutdown MeterProvider
899901
require.NoError(t, handledError)
900902
}
903+
904+
func TestExemplars(t *testing.T) {
905+
attrsOpt := otelmetric.WithAttributes(
906+
attribute.Key("A").String("B"),
907+
attribute.Key("C").String("D"),
908+
attribute.Key("E").Bool(true),
909+
attribute.Key("F").Int(42),
910+
)
911+
for _, tc := range []struct {
912+
name string
913+
recordMetrics func(ctx context.Context, meter otelmetric.Meter)
914+
expectedExemplarValue float64
915+
}{
916+
{
917+
name: "counter",
918+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
919+
counter, err := meter.Float64Counter("foo")
920+
require.NoError(t, err)
921+
counter.Add(ctx, 9, attrsOpt)
922+
},
923+
expectedExemplarValue: 9,
924+
},
925+
{
926+
name: "histogram",
927+
recordMetrics: func(ctx context.Context, meter otelmetric.Meter) {
928+
hist, err := meter.Int64Histogram("foo")
929+
require.NoError(t, err)
930+
hist.Record(ctx, 9, attrsOpt)
931+
},
932+
expectedExemplarValue: 9,
933+
},
934+
} {
935+
t.Run(tc.name, func(t *testing.T) {
936+
t.Setenv("OTEL_GO_X_EXEMPLAR", "true")
937+
// initialize registry exporter
938+
ctx := context.Background()
939+
registry := prometheus.NewRegistry()
940+
exporter, err := New(WithRegisterer(registry), WithoutTargetInfo(), WithoutScopeInfo())
941+
require.NoError(t, err)
942+
943+
// initialize resource
944+
res, err := resource.New(ctx,
945+
resource.WithAttributes(semconv.ServiceName("prometheus_test")),
946+
resource.WithAttributes(semconv.TelemetrySDKVersion("latest")),
947+
)
948+
require.NoError(t, err)
949+
res, err = resource.Merge(resource.Default(), res)
950+
require.NoError(t, err)
951+
952+
// initialize provider and meter
953+
provider := metric.NewMeterProvider(
954+
metric.WithReader(exporter),
955+
metric.WithResource(res),
956+
metric.WithView(metric.NewView(
957+
metric.Instrument{Name: "*"},
958+
metric.Stream{
959+
// filter out all attributes so they are added as filtered
960+
// attributes to the exemplar
961+
AttributeFilter: attribute.NewAllowKeysFilter(),
962+
},
963+
)),
964+
)
965+
meter := provider.Meter("meter", otelmetric.WithInstrumentationVersion("v0.1.0"))
966+
967+
// Add a sampled span context so that measurements get exemplars added
968+
sc := trace.NewSpanContext(trace.SpanContextConfig{
969+
SpanID: trace.SpanID{0o1},
970+
TraceID: trace.TraceID{0o1},
971+
TraceFlags: trace.FlagsSampled,
972+
})
973+
ctx = trace.ContextWithSpanContext(ctx, sc)
974+
// Record a single observation with the exemplar
975+
tc.recordMetrics(ctx, meter)
976+
977+
// Verify that the exemplar is present in the proto version of the
978+
// prometheus metrics.
979+
got, done, err := prometheus.ToTransactionalGatherer(registry).Gather()
980+
defer done()
981+
require.NoError(t, err)
982+
983+
require.Len(t, got, 1)
984+
family := got[0]
985+
require.Len(t, family.GetMetric(), 1)
986+
metric := family.GetMetric()[0]
987+
var exemplar *dto.Exemplar
988+
switch family.GetType() {
989+
case dto.MetricType_COUNTER:
990+
exemplar = metric.GetCounter().GetExemplar()
991+
case dto.MetricType_HISTOGRAM:
992+
for _, b := range metric.GetHistogram().GetBucket() {
993+
if b.GetExemplar() != nil {
994+
exemplar = b.GetExemplar()
995+
continue
996+
}
997+
}
998+
}
999+
require.NotNil(t, exemplar)
1000+
require.Equal(t, exemplar.GetValue(), tc.expectedExemplarValue)
1001+
expectedLabels := map[string]string{
1002+
traceIDExemplarKey: "01000000000000000000000000000000",
1003+
spanIDExemplarKey: "0100000000000000",
1004+
"A": "B",
1005+
"C": "D",
1006+
"E": "true",
1007+
"F": "42",
1008+
}
1009+
require.Equal(t, len(expectedLabels), len(exemplar.GetLabel()))
1010+
for _, label := range exemplar.GetLabel() {
1011+
val, ok := expectedLabels[label.GetName()]
1012+
require.True(t, ok)
1013+
require.Equal(t, label.GetValue(), val)
1014+
}
1015+
})
1016+
}
1017+
}

exporters/prometheus/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
go.opentelemetry.io/otel/metric v1.24.0
1111
go.opentelemetry.io/otel/sdk v1.24.0
1212
go.opentelemetry.io/otel/sdk/metric v1.24.0
13+
go.opentelemetry.io/otel/trace v1.24.0
1314
google.golang.org/protobuf v1.33.0
1415
)
1516

@@ -23,7 +24,6 @@ require (
2324
github.com/pmezard/go-difflib v1.0.0 // indirect
2425
github.com/prometheus/common v0.48.0 // indirect
2526
github.com/prometheus/procfs v0.12.0 // indirect
26-
go.opentelemetry.io/otel/trace v1.24.0 // indirect
2727
golang.org/x/sys v0.18.0 // indirect
2828
gopkg.in/yaml.v3 v3.0.1 // indirect
2929
)

0 commit comments

Comments
 (0)