Prometheus exporter: handle colliding metric attribute keys (#5717)

Co-authored-by: Jack Berg <jberg@newrelic.com>
This commit is contained in:
David Ashpole 2023-09-20 16:12:15 -04:00 committed by GitHub
parent 9b081e1933
commit 9a931556fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 182 additions and 11 deletions

View File

@ -25,6 +25,7 @@ import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.internal.ThrottlingLogger;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.DoubleExemplarData;
import io.opentelemetry.sdk.metrics.data.DoublePointData;
@ -58,12 +59,18 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/** Serializes metrics into Prometheus exposition formats. */
// Adapted from
// https://github.com/prometheus/client_java/blob/master/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java
abstract class Serializer {
private static final Logger LOGGER = Logger.getLogger(Serializer.class.getName());
private static final ThrottlingLogger THROTTLING_LOGGER = new ThrottlingLogger(LOGGER);
static Serializer create(@Nullable String acceptHeader, Predicate<String> filter) {
if (acceptHeader == null) {
return new Prometheus004Serializer(filter);
@ -445,27 +452,58 @@ abstract class Serializer {
private static void writeAttributePairs(
Writer writer, boolean initialComma, Attributes attributes) throws IOException {
try {
// This logic handles colliding attribute keys by joining the values,
// separated by a semicolon. It relies on the attributes being sorted, so that
// colliding attribute keys are in subsequent iterations of the for loop.
attributes.forEach(
new BiConsumer<AttributeKey<?>, Object>() {
private boolean prefixWithComma = initialComma;
boolean initialAttribute = true;
String previousKey = "";
String previousValue = "";
@Override
public void accept(AttributeKey<?> key, Object value) {
try {
if (prefixWithComma) {
writer.write(',');
String sanitizedKey = NameSanitizer.INSTANCE.apply(key.getKey());
int compare = sanitizedKey.compareTo(previousKey);
if (compare == 0) {
// This key collides with the previous one. Append the value
// to the previous value instead of writing the key again.
writer.write(';');
} else {
prefixWithComma = true;
if (compare < 0) {
THROTTLING_LOGGER.log(
Level.WARNING,
"Dropping out-of-order attribute "
+ sanitizedKey
+ "="
+ value
+ ", which occurred after "
+ previousKey
+ ". This can occur when an alternative Attribute implementation is used.");
}
if (!initialAttribute) {
writer.write('"');
}
if (initialComma || !initialAttribute) {
writer.write(',');
}
writer.write(sanitizedKey);
writer.write("=\"");
}
writer.write(NameSanitizer.INSTANCE.apply(key.getKey()));
writer.write("=\"");
writeEscapedLabelValue(writer, value.toString());
writer.write('"');
String stringValue = value.toString();
writeEscapedLabelValue(writer, stringValue);
previousKey = sanitizedKey;
previousValue = stringValue;
initialAttribute = false;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
});
if (!attributes.isEmpty()) {
writer.write('"');
}
} catch (UncheckedIOException e) {
throw e.getCause();
}

View File

@ -11,6 +11,7 @@ import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_DOUBLE_SU
import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_HISTOGRAM;
import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_LONG_SUM;
import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE;
import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_COLLIDING_ATTRIBUTES;
import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES;
import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_NO_ATTRIBUTES;
import static io.opentelemetry.exporter.prometheus.TestConstants.LONG_GAUGE;
@ -22,15 +23,36 @@ import static io.opentelemetry.exporter.prometheus.TestConstants.NON_MONOTONIC_C
import static io.opentelemetry.exporter.prometheus.TestConstants.SUMMARY;
import static org.assertj.core.api.Assertions.assertThat;
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData;
import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData;
import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData;
import io.opentelemetry.sdk.resources.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class SerializerTest {
@RegisterExtension
private final LogCapturer logCapturer =
LogCapturer.create().captureForLogger(Serializer.class.getName());
@Test
void prometheus004() {
// Same output as prometheus client library except for these changes which are compatible with
@ -53,7 +75,8 @@ class SerializerTest {
CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES,
CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE,
DOUBLE_GAUGE_NO_ATTRIBUTES,
DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES))
DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES,
DOUBLE_GAUGE_COLLIDING_ATTRIBUTES))
.isEqualTo(
"# TYPE target info\n"
+ "# HELP target Target metadata\n"
@ -103,7 +126,11 @@ class SerializerTest {
+ "double_gauge_no_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\"} 7.0 1633950672000\n"
+ "# TYPE double_gauge_multiple_attributes_seconds gauge\n"
+ "# HELP double_gauge_multiple_attributes_seconds unused\n"
+ "double_gauge_multiple_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",animal=\"bear\",type=\"dgma\"} 8.0 1633950672000\n");
+ "double_gauge_multiple_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",animal=\"bear\",type=\"dgma\"} 8.0 1633950672000\n"
+ "# TYPE double_gauge_colliding_attributes_seconds gauge\n"
+ "# HELP double_gauge_colliding_attributes_seconds unused\n"
+ "double_gauge_colliding_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",foo_bar=\"a;b\",type=\"dgma\"} 8.0 1633950672000\n");
assertThat(logCapturer.size()).isZero();
}
@Test
@ -124,7 +151,8 @@ class SerializerTest {
CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES,
CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE,
DOUBLE_GAUGE_NO_ATTRIBUTES,
DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES))
DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES,
DOUBLE_GAUGE_COLLIDING_ATTRIBUTES))
.isEqualTo(
"# TYPE target info\n"
+ "# HELP target Target metadata\n"
@ -175,7 +203,52 @@ class SerializerTest {
+ "# TYPE double_gauge_multiple_attributes_seconds gauge\n"
+ "# HELP double_gauge_multiple_attributes_seconds unused\n"
+ "double_gauge_multiple_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",animal=\"bear\",type=\"dgma\"} 8.0 1633950672.000\n"
+ "# TYPE double_gauge_colliding_attributes_seconds gauge\n"
+ "# HELP double_gauge_colliding_attributes_seconds unused\n"
+ "double_gauge_colliding_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",foo_bar=\"a;b\",type=\"dgma\"} 8.0 1633950672.000\n"
+ "# EOF\n");
assertThat(logCapturer.size()).isZero();
}
@Test
@SuppressLogger(Serializer.class)
void outOfOrderedAttributes() {
// Alternative attributes implementation which sorts entries by the order they were added rather
// than lexicographically
// all attributes are retained, we log a warning, and b_key and b.key are not be merged
LinkedHashMap<AttributeKey<?>, Object> attributesMap = new LinkedHashMap<>();
attributesMap.put(AttributeKey.stringKey("b_key"), "val1");
attributesMap.put(AttributeKey.stringKey("a_key"), "val2");
attributesMap.put(AttributeKey.stringKey("b.key"), "val3");
Attributes attributes = new MapAttributes(attributesMap);
MetricData metricData =
ImmutableMetricData.createDoubleSum(
Resource.builder().put("kr", "vr").build(),
InstrumentationScopeInfo.builder("scope").setVersion("1.0.0").build(),
"sum",
"description",
"s",
ImmutableSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Collections.singletonList(
ImmutableDoublePointData.create(
1633947011000000000L, 1633950672000000000L, attributes, 5))));
assertThat(serialize004(metricData))
.isEqualTo(
"# TYPE target info\n"
+ "# HELP target Target metadata\n"
+ "target_info{kr=\"vr\"} 1\n"
+ "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ "otel_scope_info{otel_scope_name=\"scope\",otel_scope_version=\"1.0.0\"} 1\n"
+ "# TYPE sum_seconds_total counter\n"
+ "# HELP sum_seconds_total description\n"
+ "sum_seconds_total{otel_scope_name=\"scope\",otel_scope_version=\"1.0.0\",b_key=\"val1\",a_key=\"val2\",b_key=\"val3\"} 5.0 1633950672000\n");
logCapturer.assertContains(
"Dropping out-of-order attribute a_key=val2, which occurred after b_key. This can occur when an alternative Attribute implementation is used.");
}
private static String serialize004(MetricData... metrics) {
@ -197,4 +270,46 @@ class SerializerTest {
throw new UncheckedIOException(e);
}
}
@SuppressWarnings("unchecked")
private static class MapAttributes implements Attributes {
private final LinkedHashMap<AttributeKey<?>, Object> map;
@SuppressWarnings("NonApiType")
private MapAttributes(LinkedHashMap<AttributeKey<?>, Object> map) {
this.map = map;
}
@Nullable
@Override
public <T> T get(AttributeKey<T> key) {
return (T) map.get(key);
}
@Override
public void forEach(BiConsumer<? super AttributeKey<?>, ? super Object> consumer) {
map.forEach(consumer);
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public Map<AttributeKey<?>, Object> asMap() {
return map;
}
@Override
public AttributesBuilder toBuilder() {
throw new UnsupportedOperationException("not supported");
}
}
}

View File

@ -357,4 +357,22 @@ class TestConstants {
1633950672000000000L,
Attributes.of(TYPE, "dgma", stringKey("animal"), "bear"),
8))));
static final MetricData DOUBLE_GAUGE_COLLIDING_ATTRIBUTES =
ImmutableMetricData.createDoubleGauge(
Resource.create(Attributes.of(stringKey("kr"), "vr")),
InstrumentationScopeInfo.builder("full")
.setVersion("version")
.setAttributes(Attributes.of(stringKey("ks"), "vs"))
.build(),
"double.gauge.colliding.attributes",
"unused",
"s",
ImmutableGaugeData.create(
Collections.singletonList(
ImmutableDoublePointData.create(
1633947011000000000L,
1633950672000000000L,
Attributes.of(
TYPE, "dgma", stringKey("foo.bar"), "a", stringKey("foo_bar"), "b"),
8))));
}