diff --git a/CHANGELOG.md b/CHANGELOG.md index 2943d778ec8..d8530a763af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ The next release will require at least [Go 1.25]. - Support testing of [Go 1.26]. (#7902) +### Fixed + +- Fix dropping entire exemplar when filtered attributes exceed the 128-rune limit.(#7883) + diff --git a/exporters/prometheus/exporter.go b/exporters/prometheus/exporter.go index 0f2259af381..ac6c37f065b 100644 --- a/exporters/prometheus/exporter.go +++ b/exporters/prometheus/exporter.go @@ -12,6 +12,7 @@ import ( "slices" "strings" "sync" + "unicode/utf8" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" @@ -40,6 +41,13 @@ const ( // Registerer, rather than round-tripping them through the bridge and // exporter. bridgeScopeName = "go.opentelemetry.io/contrib/bridges/prometheus" + + exemplarRuneLimit = 128 +) + +var ( + traceKeyLen = utf8.RuneCountInString(otlptranslator.ExemplarTraceIDKey) + spanKeyLen = utf8.RuneCountInString(otlptranslator.ExemplarSpanIDKey) ) var metricsPool = sync.Pool{ @@ -304,6 +312,7 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) { addSumMetric(ch, v, m, name, kv, c.labelNamer, c.inst, ctx) case metricdata.Sum[float64]: addSumMetric(ch, v, m, name, kv, c.labelNamer, c.inst, ctx) + case metricdata.Gauge[int64]: addGaugeMetric(ch, v, m, name, kv, c.labelNamer, c.inst, ctx) case metricdata.Gauge[float64]: @@ -780,15 +789,25 @@ func addExemplars[N int64 | float64]( return m } promExemplars := make([]prometheus.Exemplar, len(exemplars)) + for i, exemplar := range exemplars { - labels, err := attributesToLabels(exemplar.FilteredAttributes, labelNamer) + traceId := hex.EncodeToString(exemplar.TraceID) + spanId := hex.EncodeToString(exemplar.SpanID) + + remaingSpace := exemplarRuneLimit - traceKeyLen - utf8.RuneCountInString( + traceId, + ) - spanKeyLen - utf8.RuneCountInString( + spanId, + ) + + labels, err := attributesToLabels(exemplar.FilteredAttributes, labelNamer, remaingSpace) if err != nil { otel.Handle(err) return m } // Overwrite any existing trace ID or span ID attributes - labels[otlptranslator.ExemplarTraceIDKey] = hex.EncodeToString(exemplar.TraceID) - labels[otlptranslator.ExemplarSpanIDKey] = hex.EncodeToString(exemplar.SpanID) + labels[otlptranslator.ExemplarTraceIDKey] = traceId + labels[otlptranslator.ExemplarSpanIDKey] = spanId promExemplars[i] = prometheus.Exemplar{ Value: float64(exemplar.Value), Timestamp: exemplar.Time, @@ -805,14 +824,26 @@ func addExemplars[N int64 | float64]( return metricWithExemplar } -func attributesToLabels(attrs []attribute.KeyValue, labelNamer otlptranslator.LabelNamer) (prometheus.Labels, error) { - labels := make(map[string]string) +func attributesToLabels( + attrs []attribute.KeyValue, + labelNamer otlptranslator.LabelNamer, + remainingSpace int, +) (prometheus.Labels, error) { + labels := make(map[string]string, len(attrs)+2) + if remainingSpace <= 0 { + return labels, nil + } for _, attr := range attrs { name, err := labelNamer.Build(string(attr.Key)) if err != nil { return nil, err } - labels[name] = attr.Value.Emit() + val := attr.Value.Emit() + attrLength := utf8.RuneCountInString(val) + utf8.RuneCountInString(name) + if attrLength <= remainingSpace { + labels[name] = val + remainingSpace -= attrLength + } } return labels, nil } diff --git a/exporters/prometheus/exporter_test.go b/exporters/prometheus/exporter_test.go index eda709be59f..96c86594e2f 100644 --- a/exporters/prometheus/exporter_test.go +++ b/exporters/prometheus/exporter_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "sync" "testing" "time" @@ -720,6 +721,14 @@ func TestPrometheusExporter(t *testing.T) { MaxSize: 10, }}, ), + metric.NewView( + metric.Instrument{Name: "test_dropped_attrs_limit"}, + metric.Stream{ + AttributeFilter: func(kv attribute.KeyValue) bool { + return kv.Key != "long_attribute" + }, + }, + ), ), ) meter := provider.Meter( @@ -1291,6 +1300,26 @@ func TestExemplars(t *testing.T) { expectedLabels: expectedNonEscapedLabels, strategy: otlptranslator.NoTranslation, }, + { + name: "exemplar overflow does not drop exemplar", + recordMetrics: func(ctx context.Context, meter otelmetric.Meter) { + hist, err := meter.Float64Histogram("exponential_histogram") + require.NoError(t, err) + + // Create attributes that exceed 128-rune limit after accounting for trace_id/span_id + // trace_id (32) + span_id (16) + "=" (1) = 49 characters + // Remaining space: 128 - 49 = 79 characters + // longVal (80 chars) + "=" = 81 chars ✓ + // 81 chars > 79, so long_value should be truncated + + longVal := strings.Repeat("B", 80) // 80 chars + + hist.Record(ctx, 0, otelmetric.WithAttributes( + attribute.String("long_value", longVal), + ), attrsOpt) + }, + expectedLabels: expectedEscapedLabels, + }, } { t.Run(tc.name, func(t *testing.T) { // initialize registry exporter