Skip to content

Commit 32669c0

Browse files
committed
Prometheus label conversion refactored to align with spec
1 parent 29523e6 commit 32669c0

File tree

7 files changed

+150
-2
lines changed

7 files changed

+150
-2
lines changed

api/all/src/main/java/io/opentelemetry/api/common/AttributeType.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,32 @@ public enum AttributeType {
1717
STRING_ARRAY,
1818
BOOLEAN_ARRAY,
1919
LONG_ARRAY,
20-
DOUBLE_ARRAY
20+
DOUBLE_ARRAY;
21+
22+
/**
23+
* Returns whether {@code this} is a primitive type or not.
24+
*
25+
* @return {@code true} if primitive, otherwise {@code false}
26+
*/
27+
public boolean isPrimitive() {
28+
if (AttributeType.STRING.equals(this)) {
29+
return true;
30+
} else if (AttributeType.BOOLEAN.equals(this)) {
31+
return true;
32+
} else if (AttributeType.LONG.equals(this)) {
33+
return true;
34+
} else if (AttributeType.DOUBLE.equals(this)) {
35+
return true;
36+
} else if (AttributeType.STRING_ARRAY.equals(this)) {
37+
return false;
38+
} else if (AttributeType.BOOLEAN_ARRAY.equals(this)) {
39+
return false;
40+
} else if (AttributeType.LONG_ARRAY.equals(this)) {
41+
return false;
42+
} else if (AttributeType.DOUBLE_ARRAY.equals(this)) {
43+
return false;
44+
} else {
45+
throw new IllegalStateException(("Unrecognized attribute type: " + this));
46+
}
47+
}
2148
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.common;
7+
8+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
9+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
10+
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.CsvSource;
13+
import org.junit.jupiter.params.provider.EnumSource;
14+
15+
final class AttributeTypeTest {
16+
17+
@ParameterizedTest
18+
@CsvSource(
19+
value = {
20+
"STRING, true",
21+
"BOOLEAN, true",
22+
"LONG, true",
23+
"DOUBLE, true",
24+
"STRING_ARRAY, false",
25+
"BOOLEAN_ARRAY, false",
26+
"LONG_ARRAY, false",
27+
"DOUBLE_ARRAY, false",
28+
},
29+
delimiterString = ",")
30+
void isPrimitive(AttributeType type, boolean expected) {
31+
assertThat(type.isPrimitive()).isEqualTo(expected);
32+
}
33+
34+
@ParameterizedTest
35+
@EnumSource(AttributeType.class)
36+
void isPrimitive_CoversAllValues(AttributeType type) {
37+
assertDoesNotThrow(type::isPrimitive);
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
Comparing source compatibility of opentelemetry-api-1.50.0-SNAPSHOT.jar against opentelemetry-api-1.49.0.jar
2+
*** MODIFIED ENUM: PUBLIC FINAL io.opentelemetry.api.common.AttributeType (compatible)
3+
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
4+
+++ NEW METHOD: PUBLIC(+) boolean isPrimitive()
25
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.logs.LogRecordBuilder (not serializable)
36
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
47
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.logs.LogRecordBuilder setEventName(java.lang.String)

exporters/prometheus/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
implementation(project(":exporters:common"))
1414
implementation(project(":sdk-extensions:autoconfigure-spi"))
1515
implementation("io.prometheus:prometheus-metrics-exporter-httpserver")
16+
implementation("com.fasterxml.jackson.core:jackson-databind")
1617

1718
compileOnly("com.google.auto.value:auto-value-annotations")
1819

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

+27-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
1010
import static java.util.Objects.requireNonNull;
1111

12+
import com.fasterxml.jackson.core.JsonProcessingException;
13+
import com.fasterxml.jackson.databind.ObjectMapper;
1214
import io.opentelemetry.api.common.AttributeKey;
15+
import io.opentelemetry.api.common.AttributeType;
1316
import io.opentelemetry.api.common.Attributes;
1417
import io.opentelemetry.api.trace.SpanContext;
1518
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
@@ -76,6 +79,7 @@ final class Otel2PrometheusConverter {
7679
private static final String OTEL_SCOPE_NAME = "otel_scope_name";
7780
private static final String OTEL_SCOPE_VERSION = "otel_scope_version";
7881
private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1);
82+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
7983
static final int MAX_CACHE_SIZE = 10;
8084

8185
private final boolean otelScopeEnabled;
@@ -472,7 +476,9 @@ private Labels convertAttributes(
472476

473477
Map<String, String> labelNameToValue = new HashMap<>();
474478
attributes.forEach(
475-
(key, value) -> labelNameToValue.put(sanitizeLabelName(key.getKey()), value.toString()));
479+
(key, value) ->
480+
labelNameToValue.put(
481+
sanitizeLabelName(key.getKey()), toLabelValue(key.getType(), value)));
476482

477483
for (int i = 0; i < additionalAttributes.length; i += 2) {
478484
labelNameToValue.putIfAbsent(
@@ -642,4 +648,24 @@ private static String typeString(MetricSnapshot snapshot) {
642648
// Simple helper for a log message.
643649
return snapshot.getClass().getSimpleName().replace("Snapshot", "").toLowerCase(Locale.ENGLISH);
644650
}
651+
652+
private static String toLabelValue(AttributeType type, Object attributeValue) {
653+
if (type.isPrimitive()) {
654+
return attributeValue.toString();
655+
} else {
656+
return maybeToJson(attributeValue);
657+
}
658+
}
659+
660+
private static String maybeToJson(Object attributeValue) {
661+
try {
662+
return OBJECT_MAPPER.writeValueAsString(attributeValue);
663+
} catch (JsonProcessingException e) {
664+
LOGGER.log(
665+
Level.WARNING,
666+
"Label value couldn't be serialized, toString() is being used as fallback value...",
667+
e);
668+
return attributeValue.toString();
669+
}
670+
}
645671
}

exporters/prometheus/src/module/java/module-info.java

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
requires transitive io.opentelemetry.sdk.metrics;
66
requires jdk.httpserver;
77
requires java.logging;
8+
requires com.fasterxml.jackson.databind;
89
}

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

+51
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.assertj.core.api.Assertions.assertThat;
1010
import static org.assertj.core.api.Assertions.assertThatCode;
1111

12+
import io.opentelemetry.api.common.AttributeKey;
1213
import io.opentelemetry.api.common.Attributes;
1314
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
1415
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
@@ -137,6 +138,56 @@ void prometheusNameCollisionTest_Issue6277() {
137138
assertThatCode(() -> converter.convert(metricData)).doesNotThrowAnyException();
138139
}
139140

141+
@Test
142+
void labelValueSerialization_Primitives() {
143+
Attributes attributes =
144+
Attributes.builder()
145+
.put(AttributeKey.stringKey("stringKey"), "stringValue")
146+
.put(AttributeKey.booleanKey("booleanKey"), true)
147+
.put(AttributeKey.longKey("longKey"), Long.MAX_VALUE)
148+
.put(AttributeKey.doubleKey("doubleKey"), 0.12345)
149+
.build();
150+
MetricData metricData =
151+
createSampleMetricData("sample", "1", MetricDataType.LONG_SUM, attributes, null);
152+
153+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
154+
155+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("stringKey"))
156+
.isEqualTo("stringValue");
157+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("booleanKey"))
158+
.isEqualTo("true");
159+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("longKey"))
160+
.isEqualTo("9223372036854775807");
161+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("doubleKey"))
162+
.isEqualTo("0.12345");
163+
}
164+
165+
@Test
166+
void labelValueSerialization_NonPrimitives() {
167+
Attributes attributes =
168+
Attributes.builder()
169+
.put(
170+
AttributeKey.stringArrayKey("stringKey"),
171+
Arrays.asList("stringValue1", "stringValue2"))
172+
.put(AttributeKey.booleanArrayKey("booleanKey"), Arrays.asList(true, false))
173+
.put(AttributeKey.longArrayKey("longKey"), Arrays.asList(12345L, 6789L))
174+
.put(AttributeKey.doubleArrayKey("doubleKey"), Arrays.asList(0.12345, 0.6789))
175+
.build();
176+
MetricData metricData =
177+
createSampleMetricData("sample", "1", MetricDataType.LONG_SUM, attributes, null);
178+
179+
MetricSnapshots snapshots = converter.convert(Collections.singletonList(metricData));
180+
181+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("stringKey"))
182+
.isEqualTo("[\"stringValue1\",\"stringValue2\"]");
183+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("booleanKey"))
184+
.isEqualTo("[true,false]");
185+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("longKey"))
186+
.isEqualTo("[12345,6789]");
187+
assertThat(snapshots.get(0).getDataPoints().get(0).getLabels().get("doubleKey"))
188+
.isEqualTo("[0.12345,0.6789]");
189+
}
190+
140191
private static Stream<Arguments> resourceAttributesAdditionArgs() {
141192
List<Arguments> arguments = new ArrayList<>();
142193

0 commit comments

Comments
 (0)