Skip to content

Commit e2e4776

Browse files
committed
Limit exemplar label characters to conform to Prometheus limits
1 parent 1b207c6 commit e2e4776

2 files changed

Lines changed: 130 additions & 8 deletions

File tree

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ final class Otel2PrometheusConverter {
7575

7676
private static final Logger LOGGER = Logger.getLogger(Otel2PrometheusConverter.class.getName());
7777
private static final ThrottlingLogger THROTTLING_LOGGER = new ThrottlingLogger(LOGGER);
78+
// Prometheus limits the total UTF-8 character count across all exemplar label names and values
79+
// to 128. See https://github.com/open-telemetry/opentelemetry-java/issues/6770
80+
static final int EXEMPLAR_MAX_LABEL_SET_LENGTH = 128;
7881
private static final String OTEL_SCOPE_NAME = "otel_scope_name";
7982
private static final String OTEL_SCOPE_VERSION = "otel_scope_version";
8083
private static final String OTEL_SCOPE_SCHEMA_URL = "otel_scope_schema_url";
@@ -418,26 +421,58 @@ private Exemplars convertDoubleExemplars(List<DoubleExemplarData> exemplars) {
418421
private Exemplar convertExemplar(double value, ExemplarData exemplar) {
419422
SpanContext spanContext = exemplar.getSpanContext();
420423
if (spanContext.isValid()) {
421-
return new Exemplar(
422-
value,
424+
Labels labels =
423425
convertAttributes(
424426
null, // resource attributes are only copied for point's attributes
425427
null, // scope attributes are only needed for point's attributes
426428
exemplar.getFilteredAttributes(),
427429
"trace_id",
428430
spanContext.getTraceId(),
429431
"span_id",
430-
spanContext.getSpanId()),
431-
exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
432+
spanContext.getSpanId());
433+
if (labelSetLength(labels) > EXEMPLAR_MAX_LABEL_SET_LENGTH) {
434+
// Drop filtered attributes to stay within Prometheus 128-char exemplar label limit,
435+
// keeping trace_id and span_id which are the most valuable for correlation.
436+
THROTTLING_LOGGER.log(
437+
Level.WARNING,
438+
"Exemplar attributes exceeded Prometheus limit of "
439+
+ EXEMPLAR_MAX_LABEL_SET_LENGTH
440+
+ " UTF-8 characters; dropping filtered attributes.");
441+
labels =
442+
convertAttributes(
443+
null, // resource attributes are only copied for point's attributes
444+
null, // scope attributes are only needed for point's attributes
445+
Attributes.empty(),
446+
"trace_id",
447+
spanContext.getTraceId(),
448+
"span_id",
449+
spanContext.getSpanId());
450+
}
451+
return new Exemplar(value, labels, exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
432452
} else {
433-
return new Exemplar(
434-
value,
453+
Labels labels =
435454
convertAttributes(
436455
null, // resource attributes are only copied for point's attributes
437456
null, // scope attributes are only needed for point's attributes
438-
exemplar.getFilteredAttributes()),
439-
exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
457+
exemplar.getFilteredAttributes());
458+
if (labelSetLength(labels) > EXEMPLAR_MAX_LABEL_SET_LENGTH) {
459+
THROTTLING_LOGGER.log(
460+
Level.WARNING,
461+
"Exemplar attributes exceeded Prometheus limit of "
462+
+ EXEMPLAR_MAX_LABEL_SET_LENGTH
463+
+ " UTF-8 characters; dropping filtered attributes.");
464+
labels = Labels.EMPTY;
465+
}
466+
return new Exemplar(value, labels, exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
467+
}
468+
}
469+
470+
private static int labelSetLength(Labels labels) {
471+
int length = 0;
472+
for (int i = 0; i < labels.size(); i++) {
473+
length += labels.getName(i).length() + labels.getValue(i).length();
440474
}
475+
return length;
441476
}
442477

443478
private InfoSnapshot makeTargetInfo(Resource resource) {

exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverterTest.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
import io.opentelemetry.api.common.Attributes;
2121
import io.opentelemetry.api.common.KeyValue;
2222
import io.opentelemetry.api.common.Value;
23+
import io.opentelemetry.api.trace.SpanContext;
24+
import io.opentelemetry.api.trace.TraceFlags;
25+
import io.opentelemetry.api.trace.TraceState;
2326
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
2427
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
2528
import io.opentelemetry.sdk.metrics.data.MetricData;
2629
import io.opentelemetry.sdk.metrics.data.MetricDataType;
30+
import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoubleExemplarData;
2731
import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData;
2832
import io.opentelemetry.sdk.metrics.internal.data.ImmutableExponentialHistogramBuckets;
2933
import io.opentelemetry.sdk.metrics.internal.data.ImmutableExponentialHistogramData;
@@ -555,4 +559,87 @@ void validateCacheIsBounded() {
555559
// it never saw those resources before.
556560
assertThat(predicateCalledCount.get()).isEqualTo(2);
557561
}
562+
563+
@Test
564+
void exemplarLabelsWithinLimit() {
565+
SpanContext spanContext =
566+
SpanContext.create(
567+
"00000000000000000000000000000001",
568+
"0000000000000001",
569+
TraceFlags.getSampled(),
570+
TraceState.getDefault());
571+
ImmutableDoubleExemplarData exemplar =
572+
(ImmutableDoubleExemplarData)
573+
ImmutableDoubleExemplarData.create(
574+
Attributes.of(stringKey("short"), "val"),
575+
1000L,
576+
spanContext,
577+
1.0);
578+
579+
MetricData metricData =
580+
ImmutableMetricData.createDoubleGauge(
581+
Resource.getDefault(),
582+
InstrumentationScopeInfo.create("test"),
583+
"my.gauge",
584+
"desc",
585+
"unit",
586+
ImmutableGaugeData.create(
587+
Collections.singletonList(
588+
ImmutableDoublePointData.create(
589+
0, 1000, Attributes.empty(), 1.0, Collections.singletonList(exemplar)))));
590+
591+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
592+
assertThat(snapshots).isNotNull();
593+
// Labels within limit — both trace/span and filtered attribute should be present
594+
io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot point =
595+
(io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot)
596+
snapshots.get(0).getDataPoints().get(0);
597+
Labels exemplarLabels = point.getExemplar().getLabels();
598+
assertThat(exemplarLabels.get("trace_id")).isEqualTo(spanContext.getTraceId());
599+
assertThat(exemplarLabels.get("span_id")).isEqualTo(spanContext.getSpanId());
600+
assertThat(exemplarLabels.get("short")).isEqualTo("val");
601+
}
602+
603+
@Test
604+
void exemplarLabelsExceedingLimitDropsFilteredAttributes() {
605+
SpanContext spanContext =
606+
SpanContext.create(
607+
"00000000000000000000000000000001",
608+
"0000000000000001",
609+
TraceFlags.getSampled(),
610+
TraceState.getDefault());
611+
// Build a filtered attribute whose name+value alone would push total over 128
612+
String longValue = "x".repeat(100);
613+
ImmutableDoubleExemplarData exemplar =
614+
(ImmutableDoubleExemplarData)
615+
ImmutableDoubleExemplarData.create(
616+
Attributes.of(stringKey("long_attr"), longValue),
617+
1000L,
618+
spanContext,
619+
1.0);
620+
621+
MetricData metricData =
622+
ImmutableMetricData.createDoubleGauge(
623+
Resource.getDefault(),
624+
InstrumentationScopeInfo.create("test"),
625+
"my.gauge",
626+
"desc",
627+
"unit",
628+
ImmutableGaugeData.create(
629+
Collections.singletonList(
630+
ImmutableDoublePointData.create(
631+
0, 1000, Attributes.empty(), 1.0, Collections.singletonList(exemplar)))));
632+
633+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
634+
assertThat(snapshots).isNotNull();
635+
io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot point =
636+
(io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot)
637+
snapshots.get(0).getDataPoints().get(0);
638+
Labels exemplarLabels = point.getExemplar().getLabels();
639+
// trace_id and span_id are preserved
640+
assertThat(exemplarLabels.get("trace_id")).isEqualTo(spanContext.getTraceId());
641+
assertThat(exemplarLabels.get("span_id")).isEqualTo(spanContext.getSpanId());
642+
// filtered attribute is dropped to stay within limit
643+
assertThat(exemplarLabels.get("long_attr")).isNull();
644+
}
558645
}

0 commit comments

Comments
 (0)