diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java index bd1dcdaf2..6e93f5de7 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java @@ -46,6 +46,7 @@ public class ExporterOpenTelemetryProperties { private static final String SERVICE_VERSION = "service_version"; private static final String RESOURCE_ATTRIBUTES = "resource_attributes"; // otel.resource.attributes + private static final String PRESERVE_NAMES = "preserve_names"; private static final String PREFIX = "io.prometheus.exporter.opentelemetry"; @Nullable private final String endpoint; @@ -58,6 +59,7 @@ public class ExporterOpenTelemetryProperties { @Nullable private final String serviceInstanceId; @Nullable private final String serviceVersion; private final Map resourceAttributes; + @Nullable private final Boolean preserveNames; private ExporterOpenTelemetryProperties( @Nullable String protocol, @@ -69,7 +71,8 @@ private ExporterOpenTelemetryProperties( @Nullable String serviceNamespace, @Nullable String serviceInstanceId, @Nullable String serviceVersion, - Map resourceAttributes) { + Map resourceAttributes, + @Nullable Boolean preserveNames) { this.protocol = protocol; this.endpoint = endpoint; this.headers = headers; @@ -80,6 +83,7 @@ private ExporterOpenTelemetryProperties( this.serviceInstanceId = serviceInstanceId; this.serviceVersion = serviceVersion; this.resourceAttributes = resourceAttributes; + this.preserveNames = preserveNames; } @Nullable @@ -130,6 +134,16 @@ public Map getResourceAttributes() { return resourceAttributes; } + /** + * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}). + * When {@code false} (default), standard OTel name normalization is applied (stripping unit + * suffix). + */ + @Nullable + public Boolean getPreserveNames() { + return preserveNames; + } + /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -147,6 +161,7 @@ static ExporterOpenTelemetryProperties load(PropertySource propertySource) String serviceVersion = Util.loadString(PREFIX, SERVICE_VERSION, propertySource); Map resourceAttributes = Util.loadMap(PREFIX, RESOURCE_ATTRIBUTES, propertySource); + Boolean preserveNames = Util.loadBoolean(PREFIX, PRESERVE_NAMES, propertySource); return new ExporterOpenTelemetryProperties( protocol, endpoint, @@ -157,7 +172,8 @@ static ExporterOpenTelemetryProperties load(PropertySource propertySource) serviceNamespace, serviceInstanceId, serviceVersion, - resourceAttributes); + resourceAttributes, + preserveNames); } public static Builder builder() { @@ -176,6 +192,7 @@ public static class Builder { @Nullable private String serviceInstanceId; @Nullable private String serviceVersion; private final Map resourceAttributes = new HashMap<>(); + @Nullable private Boolean preserveNames; private Builder() {} @@ -318,6 +335,15 @@ public Builder resourceAttribute(String name, String value) { return this; } + /** + * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}). + * When {@code false} (default), standard OTel name normalization is applied. + */ + public Builder preserveNames(boolean preserveNames) { + this.preserveNames = preserveNames; + return this; + } + public ExporterOpenTelemetryProperties build() { return new ExporterOpenTelemetryProperties( protocol, @@ -329,7 +355,8 @@ public ExporterOpenTelemetryProperties build() { serviceNamespace, serviceInstanceId, serviceVersion, - resourceAttributes); + resourceAttributes, + preserveNames); } } } diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java index 8f122d3ee..727647e2e 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.java @@ -41,6 +41,7 @@ public static class Builder { @Nullable String serviceInstanceId; @Nullable String serviceVersion; final Map resourceAttributes = new HashMap<>(); + @Nullable Boolean preserveNames; private Builder(PrometheusProperties config) { this.config = config; @@ -194,6 +195,15 @@ public Builder resourceAttribute(String name, String value) { return this; } + /** + * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}). + * When {@code false} (default), standard OTel name normalization is applied. + */ + public Builder preserveNames(boolean preserveNames) { + this.preserveNames = preserveNames; + return this; + } + public OpenTelemetryExporter buildAndStart() { if (registry == null) { registry = PrometheusRegistry.defaultRegistry; diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java index e0c6a0fa9..2ea96e3c3 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java @@ -38,8 +38,10 @@ static MetricReader createReader( instrumentationScopeInfo); MetricReader reader = requireNonNull(readerRef.get()); + boolean preserveNames = resolvePreserveNames(builder, config); reader.register( - new PrometheusMetricProducer(registry, instrumentationScopeInfo, getResourceField(sdk))); + new PrometheusMetricProducer( + registry, instrumentationScopeInfo, getResourceField(sdk), preserveNames)); return reader; } @@ -107,6 +109,15 @@ private static Attributes otelResourceAttributes( return builder.build(); } + static boolean resolvePreserveNames( + OpenTelemetryExporter.Builder builder, PrometheusProperties config) { + if (builder.preserveNames != null) { + return builder.preserveNames; + } + Boolean fromConfig = config.getExporterOpenTelemetryProperties().getPreserveNames(); + return fromConfig != null && fromConfig; + } + static Resource getResourceField(AutoConfiguredOpenTelemetrySdk sdk) { try { Method method = AutoConfiguredOpenTelemetrySdk.class.getDeclaredMethod("getResource"); diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java index 9344fc4db..886cdd85c 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java @@ -29,14 +29,17 @@ class PrometheusMetricProducer implements CollectionRegistration { private final PrometheusRegistry registry; private final Resource resource; private final InstrumentationScopeInfo instrumentationScopeInfo; + private final boolean preserveNames; public PrometheusMetricProducer( PrometheusRegistry registry, InstrumentationScopeInfo instrumentationScopeInfo, - Resource resource) { + Resource resource, + boolean preserveNames) { this.registry = registry; this.instrumentationScopeInfo = instrumentationScopeInfo; this.resource = resource; + this.preserveNames = preserveNames; } @Override @@ -57,7 +60,8 @@ public Collection collectAllMetrics() { new MetricDataFactory( resourceWithTargetInfo, scopeFromInfo != null ? scopeFromInfo : instrumentationScopeInfo, - System.currentTimeMillis()); + System.currentTimeMillis(), + preserveNames); for (MetricSnapshot snapshot : snapshots) { if (snapshot instanceof CounterSnapshot) { addUnlessNull(result, factory.create((CounterSnapshot) snapshot)); diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/MetricDataFactory.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/MetricDataFactory.java index 78ecb0ebe..576ba05c7 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/MetricDataFactory.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/MetricDataFactory.java @@ -17,14 +17,17 @@ public class MetricDataFactory { private final Resource resource; private final InstrumentationScopeInfo instrumentationScopeInfo; private final long currentTimeMillis; + private final boolean preserveNames; public MetricDataFactory( Resource resource, InstrumentationScopeInfo instrumentationScopeInfo, - long currentTimeMillis) { + long currentTimeMillis, + boolean preserveNames) { this.resource = resource; this.instrumentationScopeInfo = instrumentationScopeInfo; this.currentTimeMillis = currentTimeMillis; + this.preserveNames = preserveNames; } @Nullable @@ -36,7 +39,8 @@ public MetricData create(CounterSnapshot snapshot) { snapshot.getMetadata(), new PrometheusCounter(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } @Nullable @@ -48,7 +52,8 @@ public MetricData create(GaugeSnapshot snapshot) { snapshot.getMetadata(), new PrometheusGauge(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } @Nullable @@ -60,13 +65,15 @@ public MetricData create(HistogramSnapshot snapshot) { snapshot.getMetadata(), new PrometheusNativeHistogram(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } else if (firstDataPoint.hasClassicHistogramData()) { return new PrometheusMetricData<>( snapshot.getMetadata(), new PrometheusClassicHistogram(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } } return null; @@ -81,7 +88,8 @@ public MetricData create(SummarySnapshot snapshot) { snapshot.getMetadata(), new PrometheusSummary(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } @Nullable @@ -93,7 +101,8 @@ public MetricData create(InfoSnapshot snapshot) { snapshot.getMetadata(), new PrometheusInfo(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } @Nullable @@ -105,7 +114,8 @@ public MetricData create(StateSetSnapshot snapshot) { snapshot.getMetadata(), new PrometheusStateSet(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } @Nullable @@ -117,6 +127,7 @@ public MetricData create(UnknownSnapshot snapshot) { snapshot.getMetadata(), new PrometheusUnknown(snapshot, currentTimeMillis), instrumentationScopeInfo, - resource); + resource, + preserveNames); } } diff --git a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/PrometheusMetricData.java b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/PrometheusMetricData.java index 20603123c..004bbfe45 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/PrometheusMetricData.java +++ b/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/PrometheusMetricData.java @@ -23,10 +23,12 @@ class PrometheusMetricData> implements MetricData { MetricMetadata metricMetadata, T data, InstrumentationScopeInfo instrumentationScopeInfo, - Resource resource) { + Resource resource, + boolean preserveNames) { this.instrumentationScopeInfo = instrumentationScopeInfo; this.resource = resource; - this.name = getNameWithoutUnit(metricMetadata); + this.name = + preserveNames ? metricMetadata.getOriginalName() : getNameWithoutUnit(metricMetadata); this.description = metricMetadata.getHelp(); this.unit = convertUnit(metricMetadata.getUnit()); this.data = data; diff --git a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/ExportTest.java b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/ExportTest.java index e134b2373..0332eb574 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/ExportTest.java +++ b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/ExportTest.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.assertj.MetricAssert; import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; import io.prometheus.metrics.core.metrics.Counter; import io.prometheus.metrics.core.metrics.Gauge; @@ -23,6 +24,7 @@ import io.prometheus.metrics.model.snapshots.Unit; import io.prometheus.metrics.model.snapshots.UnknownSnapshot; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -47,7 +49,8 @@ void setUp() throws IllegalAccessException, NoSuchFieldException { new PrometheusMetricProducer( registry, InstrumentationScopeInfo.create("test"), - Resource.create(Attributes.builder().put("staticRes", "value").build())); + Resource.create(Attributes.builder().put("staticRes", "value").build()), + false); reader.register(prometheusMetricProducer); } @@ -324,6 +327,60 @@ void metricsWithoutDataPointsAreNotExported() { assertThat(metrics).isEmpty(); } + @Test + void preserveNamesWithUnit() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + reader.register( + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true)); + + Counter.builder().name("req").unit(Unit.BYTES).register(preserveRegistry).inc(); + + List metrics = new ArrayList<>(reader.collectAllMetrics()); + assertThat(metrics).hasSize(1); + OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("req").hasUnit("By"); + } + + @Test + void preserveNamesWithUnitAlreadyInName() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + reader.register( + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true)); + + Counter.builder().name("req_bytes").unit(Unit.BYTES).register(preserveRegistry).inc(); + + List metrics = new ArrayList<>(reader.collectAllMetrics()); + assertThat(metrics).hasSize(1); + OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("req_bytes").hasUnit("By"); + } + + @Test + void preserveNamesWithoutUnit() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + reader.register( + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true)); + + Counter.builder().name("events_total").register(preserveRegistry).inc(); + + List metrics = new ArrayList<>(reader.collectAllMetrics()); + assertThat(metrics).hasSize(1); + OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("events_total"); + } + private MetricAssert metricAssert() { List metrics = testing.getMetrics(); assertThat(metrics).hasSize(1); diff --git a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java index 5a9103565..a81aec440 100644 --- a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java +++ b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfigTest.java @@ -8,6 +8,7 @@ import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.prometheus.metrics.config.ExporterOpenTelemetryProperties; +import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.config.PrometheusPropertiesLoader; import java.util.Collections; import java.util.HashMap; @@ -17,6 +18,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -289,6 +291,31 @@ void properties(String name, TestCase testCase) { } } + @Test + void resolvePreserveNamesFromBuilder() { + OpenTelemetryExporter.Builder builder = OpenTelemetryExporter.builder(); + builder.preserveNames(true); + PrometheusProperties config = PrometheusProperties.get(); + assertThat(OtelAutoConfig.resolvePreserveNames(builder, config)).isTrue(); + } + + @Test + void resolvePreserveNamesDefault() { + OpenTelemetryExporter.Builder builder = OpenTelemetryExporter.builder(); + PrometheusProperties config = PrometheusProperties.get(); + assertThat(OtelAutoConfig.resolvePreserveNames(builder, config)).isFalse(); + } + + @Test + void resolvePreserveNamesFromConfig() { + OpenTelemetryExporter.Builder builder = OpenTelemetryExporter.builder(); + ExporterOpenTelemetryProperties otelProps = + ExporterOpenTelemetryProperties.builder().preserveNames(true).build(); + PrometheusProperties config = + PrometheusProperties.builder().exporterOpenTelemetryProperties(otelProps).build(); + assertThat(OtelAutoConfig.resolvePreserveNames(builder, config)).isTrue(); + } + private static ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties( TestCase testCase) { if (testCase.propertiesBuilder == null) {