Skip to content
Draft
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.5.0"
val prometheusServerVersion = "1.6.0-SNAPSHOT"
val armeriaVersion = "1.37.0"
val junitVersion = "5.14.3"
val okhttpVersion = "5.3.2"
Expand Down
1 change: 1 addition & 0 deletions exporters/prometheus/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
exclude(group = "io.prometheus", module = "prometheus-metrics-exposition-formats")
}
implementation("io.prometheus:prometheus-metrics-exposition-formats-no-protobuf")
implementation("io.prometheus:prometheus-metrics-config")

compileOnly("com.google.auto.value:auto-value-annotations")
compileOnly("com.google.errorprone:error_prone_annotations")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

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;

import io.opentelemetry.api.common.AttributeKey;
Expand Down Expand Up @@ -84,6 +83,7 @@ final class Otel2PrometheusConverter {

private final boolean otelScopeLabelsEnabled;
private final boolean targetInfoMetricEnabled;
private final TranslationStrategy translationStrategy;
@Nullable private final Predicate<String> allowedResourceAttributesFilter;

/**
Expand All @@ -92,21 +92,14 @@ final class Otel2PrometheusConverter {
*/
private final Map<Attributes, List<AttributeKey<?>>> resourceAttributesToAllowedKeysCache;

/**
* Constructor with feature flag parameters.
*
* @param otelScopeLabelsEnabled whether to add OpenTelemetry scope labels to exported metrics
* @param targetInfoMetricEnabled whether to export the target_info metric with resource
* attributes
* @param allowedResourceAttributesFilter if not {@code null}, resource attributes with keys
* matching this predicate will be added as labels on each exported metric
*/
Otel2PrometheusConverter(
boolean otelScopeLabelsEnabled,
boolean targetInfoMetricEnabled,
TranslationStrategy translationStrategy,
@Nullable Predicate<String> allowedResourceAttributesFilter) {
this.otelScopeLabelsEnabled = otelScopeLabelsEnabled;
this.targetInfoMetricEnabled = targetInfoMetricEnabled;
this.translationStrategy = translationStrategy;
this.allowedResourceAttributesFilter = allowedResourceAttributesFilter;
this.resourceAttributesToAllowedKeysCache =
allowedResourceAttributesFilter != null
Expand All @@ -122,6 +115,10 @@ boolean isTargetInfoMetricEnabled() {
return targetInfoMetricEnabled;
}

TranslationStrategy getTranslationStrategy() {
return translationStrategy;
}

@Nullable
Predicate<String> getAllowedResourceAttributesFilter() {
return allowedResourceAttributesFilter;
Expand Down Expand Up @@ -155,7 +152,8 @@ private MetricSnapshot convert(MetricData metricData) {
// Note that AggregationTemporality.DELTA should never happen
// because PrometheusMetricReader#getAggregationTemporality returns CUMULATIVE.

MetricMetadata metadata = convertMetadata(metricData);
boolean isCounter = isMonotonicSum(metricData);
MetricMetadata metadata = convertMetadata(metricData, isCounter);
InstrumentationScopeInfo scope = metricData.getInstrumentationScopeInfo();
switch (metricData.getType()) {
case LONG_GAUGE:
Expand Down Expand Up @@ -210,6 +208,17 @@ private MetricSnapshot convert(MetricData metricData) {
return null;
}

private static boolean isMonotonicSum(MetricData metricData) {
switch (metricData.getType()) {
case LONG_SUM:
return metricData.getLongSumData().isMonotonic();
case DOUBLE_SUM:
return metricData.getDoubleSumData().isMonotonic();
default:
return false;
}
}

private GaugeSnapshot convertLongGauge(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Expand Down Expand Up @@ -545,34 +554,116 @@ 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));
private String convertLabelName(String key) {
if (translationStrategy.shouldEscape()) {
return sanitizeLabelName(prometheusName(key));
}
return key;
}

private MetricMetadata convertMetadata(MetricData metricData, boolean isCounter) {
switch (translationStrategy) {
case UNDERSCORE_ESCAPING_WITH_SUFFIXES:
return convertMetadataEscapedWithSuffixes(metricData);
case UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES:
return convertMetadataEscapedWithoutSuffixes(metricData);
case NO_UTF8_ESCAPING_WITH_SUFFIXES:
return convertMetadataUtf8WithSuffixes(metricData, isCounter);
case NO_TRANSLATION:
return convertMetadataNoTranslation(metricData);
}
throw new IllegalStateException("Unknown strategy: " + translationStrategy);
}

private static MetricMetadata convertMetadata(MetricData metricData) {
String name = sanitizeMetricName(prometheusName(metricData.getName()));
/**
* Default strategy: escape names, append unit and type suffixes, collapse repeated __. Uses 3-arg
* MetricMetadata constructor so the format writer handles type suffixes (_total for counters)
* automatically — preserving backward-compatible output format.
*/
private static MetricMetadata convertMetadataEscapedWithSuffixes(MetricData metricData) {
String name = prometheusName(metricData.getName());
String help = metricData.getDescription();
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());

// Strip reserved suffixes (_total, _info, etc.) BEFORE appending unit.
// This replicates the old sanitizeMetricName behavior which ran before unit append.
name = stripReservedMetricSuffixes(name);

// Append unit suffix
if (unit != null && !name.endsWith(unit.toString())) {
name = name + "_" + unit;
}
// 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

// Collapse repeated __ introduced by escaping
while (name.contains("__")) {
name = name.replace("__", "_");
}

return new MetricMetadata(name, help, unit);
}

private static void putOrMerge(
Map<String, MetricSnapshot> snapshotsByName, MetricSnapshot snapshot) {
String name = snapshot.getMetadata().getPrometheusName();
/** Escape names but don't add any suffixes. */
private static MetricMetadata convertMetadataEscapedWithoutSuffixes(MetricData metricData) {
String name = prometheusName(metricData.getName());

// Collapse repeated __ introduced by escaping
while (name.contains("__")) {
name = name.replace("__", "_");
}

return new MetricMetadata(name, name, metricData.getDescription(), null);
}

/** Passthrough names (UTF-8 preserved), but add unit and type suffixes. */
private static MetricMetadata convertMetadataUtf8WithSuffixes(
MetricData metricData, boolean isCounter) {
String name = metricData.getName();
String help = metricData.getDescription();
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());

if (unit != null && !name.endsWith(unit.toString())) {
name = name + "_" + unit;
}

String expositionBaseName = name;
if (isCounter && !name.endsWith("_total")) {
expositionBaseName = name + "_total";
}

return new MetricMetadata(name, expositionBaseName, help, unit);
}

/** Full passthrough: no escaping, no suffixes, no unit. */
private static MetricMetadata convertMetadataNoTranslation(MetricData metricData) {
String name = metricData.getName();
return new MetricMetadata(name, name, name, metricData.getDescription(), null);
}

/**
* Strip reserved Prometheus metric name suffixes (_total, _info, _created, _bucket). This
* replicates the behavior of the old {@code PrometheusNaming.sanitizeMetricName} which was
* changed to a no-op in prometheus client_java 1.6.0.
*/
private static String stripReservedMetricSuffixes(String name) {
boolean modified = true;
while (modified) {
modified = false;
for (String suffix : PrometheusUnitsHelper.RESERVED_SUFFIXES) {
if (name.equals(suffix)) {
// Corner case: name is exactly "_total" → return "total"
return name.substring(1);
}
if (name.endsWith(suffix)) {
name = name.substring(0, name.length() - suffix.length());
modified = true;
}
}
}
return name;
}

private void putOrMerge(Map<String, MetricSnapshot> snapshotsByName, MetricSnapshot snapshot) {
String name = getMergeKey(snapshot.getMetadata());
if (snapshotsByName.containsKey(name)) {
MetricSnapshot merged = merge(snapshotsByName.get(name), snapshot);
if (merged != null) {
Expand All @@ -583,6 +674,13 @@ private static void putOrMerge(
}
}

private String getMergeKey(MetricMetadata metadata) {
if (translationStrategy.shouldEscape()) {
return metadata.getPrometheusName();
}
return metadata.getName();
}

/**
* OpenTelemetry may use the same metric name multiple times but in different instrumentation
* scopes. In that case, we try to merge the metrics. They will have different {@code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
import io.opentelemetry.sdk.metrics.export.MetricReader;
import io.prometheus.metrics.config.OpenMetrics2Properties;
import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import java.io.IOException;
Expand Down Expand Up @@ -73,6 +75,7 @@ public static PrometheusHttpServerBuilder builder() {
@Nullable HttpHandler defaultHandler,
DefaultAggregationSelector defaultAggregationSelector,
@Nullable Authenticator authenticator,
TranslationStrategy translationStrategy,
PrometheusMetricReader prometheusMetricReader) {
this.host = host;
this.port = port;
Expand All @@ -95,9 +98,21 @@ public static PrometheusHttpServerBuilder builder() {
new LinkedBlockingQueue<>(),
new DaemonThreadFactory("prometheus-http-server"));
}
// Enable OM2 format writer for non-default strategies where the converter controls
// expositionBaseName directly. For the default strategy, OM1 handles suffixes automatically.
HTTPServer.Builder httpServerBuilder;
if (translationStrategy != TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES) {
PrometheusProperties prometheusProperties =
PrometheusProperties.builder()
.openMetrics2Properties(OpenMetrics2Properties.builder().enabled(true).build())
.build();
httpServerBuilder = HTTPServer.builder(prometheusProperties);
} else {
httpServerBuilder = HTTPServer.builder();
}
try {
this.httpServer =
HTTPServer.builder()
httpServerBuilder
.hostname(host)
.port(port)
.executorService(executor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ public PrometheusHttpServerBuilder setTargetInfoMetricEnabled(boolean targetInfo
return this;
}

/**
* Sets the translation strategy for metric and label name conversion.
*
* @param translationStrategy the strategy to use
* @return this builder
* @see TranslationStrategy
*/
public PrometheusHttpServerBuilder setTranslationStrategy(
TranslationStrategy translationStrategy) {
requireNonNull(translationStrategy, "translationStrategy");
metricReaderBuilder.setTranslationStrategy(translationStrategy);
return this;
}

/**
* Set if the resource attributes should be added as labels on each exported metric.
*
Expand Down Expand Up @@ -183,6 +197,7 @@ public PrometheusHttpServer build() {
defaultHandler,
defaultAggregationSelector,
authenticator,
metricReaderBuilder.getTranslationStrategy(),
metricReaderBuilder.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public PrometheusMetricReader(
this(
allowedResourceAttributesFilter,
/* otelScopeLabelsEnabled= */ true,
/* targetInfoMetricEnabled= */ true);
/* targetInfoMetricEnabled= */ true,
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
}

/**
Expand All @@ -65,18 +66,23 @@ public PrometheusMetricReader(@Nullable Predicate<String> allowedResourceAttribu
this(
allowedResourceAttributesFilter,
/* otelScopeLabelsEnabled= */ true,
/* targetInfoMetricEnabled= */ true);
/* targetInfoMetricEnabled= */ true,
TranslationStrategy.UNDERSCORE_ESCAPING_WITH_SUFFIXES);
}

// Package-private constructor used by builder
@SuppressWarnings("InconsistentOverloads")
PrometheusMetricReader(
@Nullable Predicate<String> allowedResourceAttributesFilter,
boolean otelScopeLabelsEnabled,
boolean targetInfoMetricEnabled) {
boolean targetInfoMetricEnabled,
TranslationStrategy translationStrategy) {
this.converter =
new Otel2PrometheusConverter(
otelScopeLabelsEnabled, targetInfoMetricEnabled, allowedResourceAttributesFilter);
otelScopeLabelsEnabled,
targetInfoMetricEnabled,
translationStrategy,
allowedResourceAttributesFilter);
}

@Override
Expand Down Expand Up @@ -109,6 +115,7 @@ public String toString() {
StringJoiner joiner = new StringJoiner(",", "PrometheusMetricReader{", "}");
joiner.add("otelScopeLabelsEnabled=" + converter.isOtelScopeLabelsEnabled());
joiner.add("targetInfoMetricEnabled=" + converter.isTargetInfoMetricEnabled());
joiner.add("translationStrategy=" + converter.getTranslationStrategy());
joiner.add("allowedResourceAttributesFilter=" + converter.getAllowedResourceAttributesFilter());
return joiner.toString();
}
Expand Down
Loading
Loading