Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!-- Released section -->
<!-- Don't change this section unless doing release -->

Expand Down
43 changes: 37 additions & 6 deletions exporters/prometheus/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"slices"
"strings"
"sync"
"unicode/utf8"

"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
Expand Down
29 changes: 29 additions & 0 deletions exporters/prometheus/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down