Skip to content

Commit a0627db

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

2 files changed

Lines changed: 123 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: 80 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;
@@ -39,6 +43,7 @@
3943
import io.opentelemetry.sdk.resources.Resource;
4044
import io.prometheus.metrics.expositionformats.ExpositionFormats;
4145
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
46+
import io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot;
4247
import io.prometheus.metrics.model.snapshots.Labels;
4348
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
4449
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
@@ -555,4 +560,79 @@ void validateCacheIsBounded() {
555560
// it never saw those resources before.
556561
assertThat(predicateCalledCount.get()).isEqualTo(2);
557562
}
563+
564+
@Test
565+
void exemplarLabelsWithinLimit() {
566+
SpanContext spanContext =
567+
SpanContext.create(
568+
"00000000000000000000000000000001",
569+
"0000000000000001",
570+
TraceFlags.getSampled(),
571+
TraceState.getDefault());
572+
ImmutableDoubleExemplarData exemplar =
573+
(ImmutableDoubleExemplarData)
574+
ImmutableDoubleExemplarData.create(
575+
Attributes.of(stringKey("short"), "val"), 1000L, spanContext, 1.0);
576+
577+
MetricData metricData =
578+
ImmutableMetricData.createDoubleGauge(
579+
Resource.getDefault(),
580+
InstrumentationScopeInfo.create("test"),
581+
"my.gauge",
582+
"desc",
583+
"unit",
584+
ImmutableGaugeData.create(
585+
Collections.singletonList(
586+
ImmutableDoublePointData.create(
587+
0, 1000, Attributes.empty(), 1.0, Collections.singletonList(exemplar)))));
588+
589+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
590+
assertThat(snapshots).isNotNull();
591+
// Labels within limit — both trace/span and filtered attribute should be present
592+
GaugeDataPointSnapshot point = (GaugeDataPointSnapshot) snapshots.get(0).getDataPoints().get(0);
593+
Labels exemplarLabels = point.getExemplar().getLabels();
594+
assertThat(exemplarLabels.get("trace_id")).isEqualTo(spanContext.getTraceId());
595+
assertThat(exemplarLabels.get("span_id")).isEqualTo(spanContext.getSpanId());
596+
assertThat(exemplarLabels.get("short")).isEqualTo("val");
597+
}
598+
599+
@Test
600+
void exemplarLabelsExceedingLimitDropsFilteredAttributes() {
601+
SpanContext spanContext =
602+
SpanContext.create(
603+
"00000000000000000000000000000001",
604+
"0000000000000001",
605+
TraceFlags.getSampled(),
606+
TraceState.getDefault());
607+
// Build a filtered attribute whose name+value alone would push total over 128
608+
char[] chars = new char[100];
609+
Arrays.fill(chars, 'x');
610+
String longValue = new String(chars);
611+
ImmutableDoubleExemplarData exemplar =
612+
(ImmutableDoubleExemplarData)
613+
ImmutableDoubleExemplarData.create(
614+
Attributes.of(stringKey("long_attr"), longValue), 1000L, spanContext, 1.0);
615+
616+
MetricData metricData =
617+
ImmutableMetricData.createDoubleGauge(
618+
Resource.getDefault(),
619+
InstrumentationScopeInfo.create("test"),
620+
"my.gauge",
621+
"desc",
622+
"unit",
623+
ImmutableGaugeData.create(
624+
Collections.singletonList(
625+
ImmutableDoublePointData.create(
626+
0, 1000, Attributes.empty(), 1.0, Collections.singletonList(exemplar)))));
627+
628+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
629+
assertThat(snapshots).isNotNull();
630+
GaugeDataPointSnapshot point = (GaugeDataPointSnapshot) snapshots.get(0).getDataPoints().get(0);
631+
Labels exemplarLabels = point.getExemplar().getLabels();
632+
// trace_id and span_id are preserved
633+
assertThat(exemplarLabels.get("trace_id")).isEqualTo(spanContext.getTraceId());
634+
assertThat(exemplarLabels.get("span_id")).isEqualTo(spanContext.getSpanId());
635+
// filtered attribute is dropped to stay within limit
636+
assertThat(exemplarLabels.get("long_attr")).isNull();
637+
}
558638
}

0 commit comments

Comments
 (0)