Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dependencyManagement/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ val jmhVersion = "1.37"
val mockitoVersion = "4.11.0"
val slf4jVersion = "2.0.17"
val opencensusVersion = "0.31.1"
val prometheusServerVersion = "1.3.10"
val prometheusServerVersion = "1.5.0"
val armeriaVersion = "1.36.0"
val junitVersion = "5.14.3"
val okhttpVersion = "5.3.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.exporter.prometheus;

import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -474,7 +475,7 @@ private Labels convertAttributes(
attributes.forEach(
(key, value) ->
labelNameToValue.put(
sanitizeLabelName(key.getKey()), toLabelValue(key.getType(), value)));
convertLabelName(key.getKey()), toLabelValue(key.getType(), value)));

for (int i = 0; i < additionalAttributes.length; i += 2) {
labelNameToValue.putIfAbsent(
Expand Down Expand Up @@ -504,7 +505,7 @@ private Labels convertAttributes(
Object attributeValue = resourceAttributes.get(attributeKey);
if (attributeValue != null) {
labelNameToValue.putIfAbsent(
sanitizeLabelName(attributeKey.getKey()), attributeValue.toString());
convertLabelName(attributeKey.getKey()), attributeValue.toString());
}
}
}
Expand Down Expand Up @@ -544,14 +545,24 @@ private List<AttributeKey<?>> filterAllowedResourceAttributeKeys(@Nullable Resou
return allowedAttributeKeys;
}

/**
* Convert an attribute key to a legacy Prometheus label name. {@code prometheusName} converts
* non-standard characters (dots, dashes, etc.) to underscores, and {@code sanitizeLabelName}
* strips invalid leading prefixes.
*/
private static String convertLabelName(String key) {
return sanitizeLabelName(prometheusName(key));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeitlinger when I look into the code I see prometheusName calls escapeName(String, EscapingScheme), where EscapingScheme is an enum with values: ALLOW_UTF8, UNDERSCORE_ESCAPING, DOTS_ESCAPING, VALUE_ENCODING_ESCAPING

And I've seen some hints in the code that the metric and attribute name escaping is influenced by headers requested by the client. Do we need to escape at all or will prometheus do we want by default?

Also, we've had offline conversations about the prometheus client library automatically appending suffixes like _total and units, and not providing an opt out mechanism, which makes it problematic for us in opentelemetry-java to implement the translation strategy option. Want to confirm this is still the case with this new version. Seems a bit strange that prometheus would have facilities to produce a full unescaped utf8 metric name but still always produces metric names with the suffixes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escaping: You're right that the escaping scheme is normally determined at scrape time based on the Accept header (EscapingScheme.fromAcceptHeader()). By calling prometheusName() here, we're pre-escaping to underscores at snapshot creation time, so even if a scraper requests escaping=allow-utf-8, it would still get underscore-escaped names.

This is intentional for this PR — the goal is just to maintain backward-compatible behavior after the 1.5.0 upgrade (where sanitizeMetricName/sanitizeLabelName no longer do underscore escaping themselves). UTF-8 support will be unlocked by implementing the translation strategy option, which controls whether names are escaped to legacy format or kept as-is.

Suffixes: Yes, the Prometheus client still hardcodes _total in the text format writer for CounterSnapshot with no opt-out. Unit suffixes are already handled by the OTel exporter itself (in convertMetadata), so those aren't a problem. The _total suffix is the blocker for the translation strategy option (prometheus/client_java#1555). The plan is to add a disableSuffixAppending flag as part of experimental OpenMetrics 2.0 support (prometheus/client_java#1912), which will also unblock the OTel translation strategies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I'll leave this unresolved so I can find it easier when we come back to the translation strategy work.

}

private static MetricMetadata convertMetadata(MetricData metricData) {
String name = sanitizeMetricName(metricData.getName());
String name = sanitizeMetricName(prometheusName(metricData.getName()));
String help = metricData.getDescription();
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());
if (unit != null && !name.endsWith(unit.toString())) {
name = name + "_" + unit;
}
// Repeated __ are not allowed according to spec, although this is allowed in prometheus
// Repeated __ are discouraged according to spec, although this is allowed in prometheus, see
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
while (name.contains("__")) {
name = name.replace("__", "_");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import io.opentelemetry.sdk.resources.Resource;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
import io.prometheus.metrics.exporter.httpserver.MetricsHandler;
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
import io.prometheus.metrics.expositionformats.generated.Metrics;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -590,6 +590,7 @@ public Result authenticate(HttpExchange exchange) {
* the protobuf java bindings, and assert against the string representation.
*/
@Test
@SuppressWarnings("NonCanonicalType") // stable Metrics class extends versioned protobuf class
void histogramDefaultBase2ExponentialHistogram() throws IOException {
PrometheusHttpServer prometheusServer =
PrometheusHttpServer.builder()
Expand Down
Loading