Prometheus exporter: handle colliding metric attribute keys (#5717)
Co-authored-by: Jack Berg <jberg@newrelic.com>
This commit is contained in:
parent
9b081e1933
commit
9a931556fb
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue