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-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java index a92536036..be1d13279 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java @@ -9,27 +9,39 @@ public class OpenMetrics2Properties { private static final String PREFIX = "io.prometheus.openmetrics2"; + private static final String ENABLED = "enabled"; private static final String CONTENT_NEGOTIATION = "content_negotiation"; private static final String COMPOSITE_VALUES = "composite_values"; private static final String EXEMPLAR_COMPLIANCE = "exemplar_compliance"; private static final String NATIVE_HISTOGRAMS = "native_histograms"; + @Nullable private final Boolean enabled; @Nullable private final Boolean contentNegotiation; @Nullable private final Boolean compositeValues; @Nullable private final Boolean exemplarCompliance; @Nullable private final Boolean nativeHistograms; private OpenMetrics2Properties( + @Nullable Boolean enabled, @Nullable Boolean contentNegotiation, @Nullable Boolean compositeValues, @Nullable Boolean exemplarCompliance, @Nullable Boolean nativeHistograms) { + this.enabled = enabled; this.contentNegotiation = contentNegotiation; this.compositeValues = compositeValues; this.exemplarCompliance = exemplarCompliance; this.nativeHistograms = nativeHistograms; } + /** + * Enable the OpenMetrics 2.0 text format writer. When {@code true}, the OM2 writer is used + * instead of OM1 for OpenMetrics responses. Default is {@code false}. + */ + public boolean getEnabled() { + return enabled != null && enabled; + } + /** Gate OM2 features behind content negotiation. Default is {@code false}. */ public boolean getContentNegotiation() { return contentNegotiation != null && contentNegotiation; @@ -56,12 +68,13 @@ public boolean getNativeHistograms() { */ static OpenMetrics2Properties load(PropertySource propertySource) throws PrometheusPropertiesException { + Boolean enabled = Util.loadBoolean(PREFIX, ENABLED, propertySource); Boolean contentNegotiation = Util.loadBoolean(PREFIX, CONTENT_NEGOTIATION, propertySource); Boolean compositeValues = Util.loadBoolean(PREFIX, COMPOSITE_VALUES, propertySource); Boolean exemplarCompliance = Util.loadBoolean(PREFIX, EXEMPLAR_COMPLIANCE, propertySource); Boolean nativeHistograms = Util.loadBoolean(PREFIX, NATIVE_HISTOGRAMS, propertySource); return new OpenMetrics2Properties( - contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); + enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); } public static Builder builder() { @@ -70,6 +83,7 @@ public static Builder builder() { public static class Builder { + @Nullable private Boolean enabled; @Nullable private Boolean contentNegotiation; @Nullable private Boolean compositeValues; @Nullable private Boolean exemplarCompliance; @@ -77,6 +91,12 @@ public static class Builder { private Builder() {} + /** See {@link #getEnabled()} */ + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + /** See {@link #getContentNegotiation()} */ public Builder contentNegotiation(boolean contentNegotiation) { this.contentNegotiation = contentNegotiation; @@ -103,6 +123,7 @@ public Builder nativeHistograms(boolean nativeHistograms) { /** Enable all OpenMetrics 2.0 features */ public Builder enableAll() { + this.enabled = true; this.contentNegotiation = true; this.compositeValues = true; this.exemplarCompliance = true; @@ -112,7 +133,7 @@ public Builder enableAll() { public OpenMetrics2Properties build() { return new OpenMetrics2Properties( - contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); + enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms); } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index a9045d711..55e7d8dab 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -242,7 +242,8 @@ public Builder exporterOpenTelemetryProperties( } public Builder enableOpenMetrics2(Consumer configurator) { - OpenMetrics2Properties.Builder openMetrics2Builder = OpenMetrics2Properties.builder(); + OpenMetrics2Properties.Builder openMetrics2Builder = + OpenMetrics2Properties.builder().enabled(true); configurator.accept(openMetrics2Builder); this.openMetrics2Properties = openMetrics2Builder.build(); return this; diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java index c3a0b9fca..e7a273464 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java @@ -15,6 +15,8 @@ void load() { load( new HashMap<>( Map.of( + "io.prometheus.openmetrics2.enabled", + "true", "io.prometheus.openmetrics2.content_negotiation", "true", "io.prometheus.openmetrics2.composite_values", @@ -23,6 +25,7 @@ void load() { "true", "io.prometheus.openmetrics2.native_histograms", "true"))); + assertThat(properties.getEnabled()).isTrue(); assertThat(properties.getContentNegotiation()).isTrue(); assertThat(properties.getCompositeValues()).isTrue(); assertThat(properties.getExemplarCompliance()).isTrue(); @@ -31,6 +34,11 @@ void load() { @Test void loadInvalidValue() { + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> load(new HashMap<>(Map.of("io.prometheus.openmetrics2.enabled", "invalid")))) + .withMessage( + "io.prometheus.openmetrics2.enabled: Expecting 'true' or 'false'. Found: invalid"); assertThatExceptionOfType(PrometheusPropertiesException.class) .isThrownBy( () -> @@ -79,11 +87,13 @@ private static OpenMetrics2Properties load(Map map) { void builder() { OpenMetrics2Properties properties = OpenMetrics2Properties.builder() + .enabled(true) .contentNegotiation(true) .compositeValues(false) .exemplarCompliance(true) .nativeHistograms(false) .build(); + assertThat(properties.getEnabled()).isTrue(); assertThat(properties.getContentNegotiation()).isTrue(); assertThat(properties.getCompositeValues()).isFalse(); assertThat(properties.getExemplarCompliance()).isTrue(); @@ -93,6 +103,7 @@ void builder() { @Test void builderEnableAll() { OpenMetrics2Properties properties = OpenMetrics2Properties.builder().enableAll().build(); + assertThat(properties.getEnabled()).isTrue(); assertThat(properties.getContentNegotiation()).isTrue(); assertThat(properties.getCompositeValues()).isTrue(); assertThat(properties.getExemplarCompliance()).isTrue(); @@ -102,6 +113,7 @@ void builderEnableAll() { @Test void defaultValues() { OpenMetrics2Properties properties = OpenMetrics2Properties.builder().build(); + assertThat(properties.getEnabled()).isFalse(); assertThat(properties.getContentNegotiation()).isFalse(); assertThat(properties.getCompositeValues()).isFalse(); assertThat(properties.getExemplarCompliance()).isFalse(); diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java index c5f2f1cff..8530c988f 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java @@ -241,7 +241,7 @@ private Builder(PrometheusProperties properties) { */ @Override public Builder name(String name) { - return super.name(stripTotalSuffix(name)); + return super.nameWithOriginal(stripTotalSuffix(name), name); } @Override diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java index 011f0bb73..e782c9156 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java @@ -146,7 +146,7 @@ private Builder(PrometheusProperties config) { */ @Override public Builder name(String name) { - return super.name(stripInfoSuffix(name)); + return super.nameWithOriginal(stripInfoSuffix(name), name); } /** Throws an {@link UnsupportedOperationException} because Info metrics cannot have a unit. */ diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java index 12c48c51d..1b63004d8 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java @@ -25,8 +25,14 @@ public abstract class MetricWithFixedMetadata extends Metric { protected MetricWithFixedMetadata(Builder builder) { super(builder); + String name = makeName(builder.name, builder.unit); + if (builder.originalName == null) { + throw new IllegalArgumentException("Missing required field: name is null"); + } + String originalName = builder.originalName; + String expositionBaseName = makeExpositionBaseName(originalName, builder.unit); this.metadata = - new MetricMetadata(makeName(builder.name, builder.unit), builder.help, builder.unit); + new MetricMetadata(name, expositionBaseName, originalName, builder.help, builder.unit); this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length); } @@ -47,6 +53,18 @@ private String makeName(@Nullable String name, @Nullable Unit unit) { return name; } + private String makeExpositionBaseName(@Nullable String expositionBaseName, @Nullable Unit unit) { + if (expositionBaseName == null) { + throw new IllegalArgumentException("Missing required field: name is null"); + } + if (unit != null) { + if (!expositionBaseName.endsWith("_" + unit) && !expositionBaseName.endsWith("." + unit)) { + expositionBaseName += "_" + unit; + } + } + return expositionBaseName; + } + @Override public String getPrometheusName() { return metadata.getPrometheusName(); @@ -68,6 +86,7 @@ public abstract static class Builder, M extends MetricWi extends Metric.Builder { @Nullable private String name; + @Nullable private String originalName; @Nullable private Unit unit; @Nullable private String help; private String[] labelNames = new String[0]; @@ -82,6 +101,25 @@ public B name(String name) { throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error); } this.name = name; + this.originalName = name; + return self(); + } + + /** + * Set the metric name and original name separately. Used by Counter and Info builders which + * strip type suffixes from the name but preserve the original for exposition. + */ + protected B nameWithOriginal(String name, String originalName) { + String error = PrometheusNaming.validateMetricName(name); + if (error != null) { + throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error); + } + error = PrometheusNaming.validateMetricName(originalName); + if (error != null) { + throw new IllegalArgumentException("'" + originalName + "': Illegal metric name: " + error); + } + this.name = name; + this.originalName = originalName; return self(); } 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..de08f4317 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 @@ -47,7 +47,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 +325,69 @@ void metricsWithoutDataPointsAreNotExported() { assertThat(metrics).isEmpty(); } + @Test + void preserveNamesWithUnit() throws NoSuchFieldException, IllegalAccessException { + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + Field field = testing.getClass().getDeclaredField("metricReader"); + field.setAccessible(true); + MetricReader reader = (MetricReader) field.get(testing); + PrometheusMetricProducer preserveProducer = + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true); + reader.register(preserveProducer); + + Counter.builder().name("req").unit(Unit.BYTES).register(preserveRegistry).inc(); + + List metrics = testing.getMetrics(); + assertThat(metrics).hasSize(1); + OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("req").hasUnit("By"); + } + + @Test + void preserveNamesWithUnitAlreadyInName() throws NoSuchFieldException, IllegalAccessException { + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + Field field = testing.getClass().getDeclaredField("metricReader"); + field.setAccessible(true); + MetricReader reader = (MetricReader) field.get(testing); + PrometheusMetricProducer preserveProducer = + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true); + reader.register(preserveProducer); + + Counter.builder().name("req_bytes").unit(Unit.BYTES).register(preserveRegistry).inc(); + + List metrics = testing.getMetrics(); + assertThat(metrics).hasSize(1); + OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("req_bytes").hasUnit("By"); + } + + @Test + void preserveNamesWithoutUnit() throws NoSuchFieldException, IllegalAccessException { + PrometheusRegistry preserveRegistry = new PrometheusRegistry(); + Field field = testing.getClass().getDeclaredField("metricReader"); + field.setAccessible(true); + MetricReader reader = (MetricReader) field.get(testing); + PrometheusMetricProducer preserveProducer = + new PrometheusMetricProducer( + preserveRegistry, + InstrumentationScopeInfo.create("test"), + Resource.create(Attributes.builder().put("staticRes", "value").build()), + true); + reader.register(preserveProducer); + + Counter.builder().name("events_total").register(preserveRegistry).inc(); + + List metrics = testing.getMetrics(); + 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) { diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java index d714fb5cd..5e58275ca 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java @@ -296,7 +296,12 @@ private void setMetadataUnlessEmpty( if (nameSuffix == null) { builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme)); } else { - builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix); + String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme); + if (expositionBaseName.endsWith(nameSuffix)) { + builder.setName(expositionBaseName); + } else { + builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix); + } } if (metadata.getHelp() != null) { builder.setHelp(metadata.getHelp()); diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java index ea2b294a2..a4a7088b8 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java @@ -76,11 +76,7 @@ public ExpositionFormatWriter findWriter(@Nullable String acceptHeader) { } private boolean isOpenMetrics2Enabled() { - OpenMetrics2Properties props = openMetrics2TextFormatWriter.getOpenMetrics2Properties(); - return props.getContentNegotiation() - || props.getCompositeValues() - || props.getExemplarCompliance() - || props.getNativeHistograms(); + return openMetrics2TextFormatWriter.getOpenMetrics2Properties().getEnabled(); } public PrometheusProtobufWriter getPrometheusProtobufWriter() { diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 0e03a112a..53df3dc49 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -6,7 +6,7 @@ import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp; -import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; +import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; import io.prometheus.metrics.config.EscapingScheme; @@ -40,9 +40,9 @@ import javax.annotation.Nullable; /** - * Write the OpenMetrics 2.0 text format. This is currently a skeleton implementation that produces - * identical output to OpenMetrics 1.0, with infrastructure for future OM2 features. This is - * experimental and subject to change as the OpenMetrics * 2.0 specification evolves. */ @@ -171,22 +171,24 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "counter", metadata, scheme); + // OM2: use the name as provided by the user, no _total appending + String counterName = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, counterName, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); + writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, counterName, data, scheme); } } private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "gauge", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "gauge", metadata); for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -199,20 +201,21 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); + String name = getExpositionBaseMetadataName(metadata, scheme); if (snapshot.isGaugeHistogram()) { - writeMetadata(writer, "gaugehistogram", metadata, scheme); + writeMetadataWithName(writer, name, "gaugehistogram", metadata); writeClassicHistogramBuckets( - writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); + writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); } else { - writeMetadata(writer, "histogram", metadata, scheme); + writeMetadataWithName(writer, name, "histogram", metadata); writeClassicHistogramBuckets( - writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); + writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme); } } private void writeClassicHistogramBuckets( Writer writer, - MetricMetadata metadata, + String name, String countSuffix, String sumSuffix, List dataList, @@ -225,13 +228,7 @@ private void writeClassicHistogramBuckets( for (int i = 0; i < buckets.size(); i++) { cumulativeCount += buckets.getCount(i); writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - "_bucket", - data.getLabels(), - scheme, - "le", - buckets.getUpperBound(i)); + writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i)); writeLong(writer, cumulativeCount); Exemplar exemplar; if (i == 0) { @@ -243,9 +240,9 @@ private void writeClassicHistogramBuckets( } // In OpenMetrics format, histogram _count and _sum are either both present or both absent. if (data.hasCount() && data.hasSum()) { - writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme); + writeCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme); } - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, name, data, scheme); } } @@ -263,12 +260,13 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); + String name = getExpositionBaseMetadataName(metadata, scheme); for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { continue; } if (!metadataWritten) { - writeMetadata(writer, "summary", metadata, scheme); + writeMetadataWithName(writer, name, "summary", metadata); metadataWritten = true; } Exemplars exemplars = data.getExemplars(); @@ -280,13 +278,7 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem int exemplarIndex = 1; for (Quantile quantile : data.getQuantiles()) { writeNameAndLabels( - writer, - getMetadataName(metadata, scheme), - null, - data.getLabels(), - scheme, - "quantile", - quantile.getQuantile()); + writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); writeDouble(writer, quantile.getValue()); if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { exemplarIndex = (exemplarIndex + 1) % exemplars.size(); @@ -296,18 +288,20 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem } } // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. - writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme); - writeCreated(writer, metadata, data, scheme); + writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme); + writeCreated(writer, name, data, scheme); } } private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "info", metadata, scheme); + // OM2 spec: Info MetricFamily name MUST end in _info + String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info"); + String baseName = removeSuffix(infoName, "_info"); + writeMetadataWithName(writer, baseName, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme); + writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); writer.write("1"); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } @@ -316,10 +310,11 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "stateset", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "stateset", metadata); for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { for (int i = 0; i < data.size(); i++) { - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write('{'); Labels labels = data.getLabels(); for (int j = 0; j < labels.size(); j++) { @@ -334,7 +329,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch if (!labels.isEmpty()) { writer.write(","); } - writer.write(getMetadataName(metadata, scheme)); + writer.write(name); writer.write("=\""); writeEscapedString(writer, data.getName(i)); writer.write("\"} "); @@ -351,9 +346,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "unknown", metadata, scheme); + String name = getExpositionBaseMetadataName(metadata, scheme); + writeMetadataWithName(writer, name, "unknown", metadata); for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); @@ -365,7 +361,7 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem private void writeCountAndSum( Writer writer, - MetricMetadata metadata, + String name, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, @@ -373,8 +369,7 @@ private void writeCountAndSum( EscapingScheme scheme) throws IOException { if (data.hasCount()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme); writeLong(writer, data.getCount()); if (exemplarsOnAllMetricTypesEnabled) { writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); @@ -383,19 +378,17 @@ private void writeCountAndSum( } } if (data.hasSum()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme); + writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme); writeDouble(writer, data.getSum()); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } } private void writeCreated( - Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) + Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme) throws IOException { if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme); + writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme); writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); if (data.hasScrapeTimestamp()) { writer.write(' '); @@ -466,27 +459,40 @@ private void writeScrapeTimestampAndExemplar( writer.write('\n'); } - private void writeMetadata( - Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) - throws IOException { + private void writeMetadataWithName( + Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getHelp()); writer.write('\n'); } } + + private static String ensureSuffix(String name, String suffix) { + if (name.endsWith(suffix)) { + return name; + } + return name + suffix; + } + + private static String removeSuffix(String name, String suffix) { + if (name.endsWith(suffix)) { + return name.substring(0, name.length() - suffix.length()); + } + return name; + } } diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java index 293fbfb8c..bffb60c14 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java @@ -6,6 +6,7 @@ import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp; +import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; @@ -141,13 +142,14 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "counter", metadata, scheme); + String counterName = resolveExpositionName(metadata, "_total", scheme); + String baseName = resolveBaseName(counterName, "_total"); + writeMetadataWithName(writer, baseName, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); + writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); - writeCreated(writer, metadata, data, scheme); + writeCreated(writer, baseName, data, scheme); } } @@ -274,10 +276,11 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "info", metadata, scheme); + String infoName = resolveExpositionName(metadata, "_info", scheme); + String baseName = resolveBaseName(infoName, "_info"); + writeMetadataWithName(writer, baseName, "info", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme); + writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); writer.write("1"); writeScrapeTimestampAndExemplar(writer, data, null, scheme); } @@ -363,9 +366,14 @@ private void writeCountAndSum( private void writeCreated( Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) throws IOException { + writeCreated(writer, getMetadataName(metadata, scheme), data, scheme); + } + + private void writeCreated( + Writer writer, String baseName, DataPointSnapshot data, EscapingScheme scheme) + throws IOException { if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme); + writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme); writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); if (data.hasScrapeTimestamp()) { writer.write(' '); @@ -436,27 +444,53 @@ private void writeScrapeTimestampAndExemplar( writer.write('\n'); } + /** + * Returns the full exposition name for a metric. If the original name already ends with the given + * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the + * suffix to the base name. + */ + private static String resolveExpositionName( + MetricMetadata metadata, String suffix, EscapingScheme scheme) { + String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); + if (expositionBaseName.endsWith(suffix)) { + return expositionBaseName; + } + return getMetadataName(metadata, scheme) + suffix; + } + private void writeMetadata( Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) throws IOException { + writeMetadataWithName(writer, getMetadataName(metadata, scheme), typeName, metadata); + } + + private void writeMetadataWithName( + Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writer.write(typeName); writer.write('\n'); if (metadata.getUnit() != null) { writer.write("# UNIT "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getUnit().toString()); writer.write('\n'); } if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { writer.write("# HELP "); - writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); + writeName(writer, name, NameType.Metric); writer.write(' '); writeEscapedString(writer, metadata.getHelp()); writer.write('\n'); } } + + private static String resolveBaseName(String fullName, String suffix) { + if (fullName.endsWith(suffix)) { + return fullName.substring(0, fullName.length() - suffix.length()); + } + return fullName; + } } diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java index cc9f067ba..b40dcfdf2 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java @@ -7,6 +7,7 @@ import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName; import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot; +import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; @@ -157,14 +158,17 @@ public void writeCreated(Writer writer, MetricSnapshot snapshot, EscapingScheme throws IOException { boolean metadataWritten = false; MetricMetadata metadata = snapshot.getMetadata(); + String baseName = getMetadataName(metadata, scheme); + if (snapshot instanceof CounterSnapshot) { + baseName = resolveBaseName(resolveExpositionName(metadata, "_total", scheme), "_total"); + } for (DataPointSnapshot data : snapshot.getDataPoints()) { if (data.hasCreatedTimestamp()) { if (!metadataWritten) { - writeMetadata(writer, "_created", "gauge", metadata, scheme); + writeMetadataWithFullName(writer, baseName + "_created", "gauge", metadata); metadataWritten = true; } - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme); + writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme); writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs); writeScrapeTimestampAndNewline(writer, data); } @@ -175,10 +179,10 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem throws IOException { if (!snapshot.getDataPoints().isEmpty()) { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "_total", "counter", metadata, scheme); + String counterName = resolveExpositionName(metadata, "_total", scheme); + writeMetadataWithFullName(writer, counterName, "counter", metadata); for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); + writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); writeScrapeTimestampAndNewline(writer, data); } @@ -321,10 +325,10 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) throws IOException { MetricMetadata metadata = snapshot.getMetadata(); - writeMetadata(writer, "_info", "gauge", metadata, scheme); + String infoName = resolveExpositionName(metadata, "_info", scheme); + writeMetadataWithFullName(writer, infoName, "gauge", metadata); for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { - writeNameAndLabels( - writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme); + writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); writer.write("1"); writeScrapeTimestampAndNewline(writer, data); } @@ -433,6 +437,44 @@ private void writeMetadata( writer.write('\n'); } + private void writeMetadataWithFullName( + Writer writer, String fullName, String typeString, MetricMetadata metadata) + throws IOException { + if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { + writer.write("# HELP "); + writeName(writer, fullName, NameType.Metric); + writer.write(' '); + writeEscapedHelp(writer, metadata.getHelp()); + writer.write('\n'); + } + writer.write("# TYPE "); + writeName(writer, fullName, NameType.Metric); + writer.write(' '); + writer.write(typeString); + writer.write('\n'); + } + + /** + * Returns the full exposition name for a metric. If the original name already ends with the given + * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the + * suffix to the base name. + */ + private static String resolveExpositionName( + MetricMetadata metadata, String suffix, EscapingScheme scheme) { + String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); + if (expositionBaseName.endsWith(suffix)) { + return expositionBaseName; + } + return getMetadataName(metadata, scheme) + suffix; + } + + private static String resolveBaseName(String fullName, String suffix) { + if (fullName.endsWith(suffix)) { + return fullName.substring(0, fullName.length() - suffix.length()); + } + return fullName; + } + private void writeEscapedHelp(Writer writer, String s) throws IOException { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index 35619042c..339c5dfa0 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -119,60 +119,36 @@ void testOM2DisabledByDefault() { } @Test - void testOM2EnabledWithContentNegotiation() { + void testOM2EnabledOnly() { PrometheusProperties props = PrometheusProperties.builder() - .openMetrics2Properties( - OpenMetrics2Properties.builder().contentNegotiation(true).build()) + .openMetrics2Properties(OpenMetrics2Properties.builder().enabled(true).build()) .build(); ExpositionFormats formats = ExpositionFormats.init(props); ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); - // When contentNegotiation is enabled, should return OM2 writer assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); } @Test - void testOM2EnabledWithCompositeValues() { - PrometheusProperties props = - PrometheusProperties.builder() - .openMetrics2Properties(OpenMetrics2Properties.builder().compositeValues(true).build()) - .build(); - ExpositionFormats formats = ExpositionFormats.init(props); - ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); - assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); - } - - @Test - void testOM2EnabledWithExemplarCompliance() { + void testOM2NotEnabledByFeatureFlagAlone() { + // Feature flags without enabled=true should not activate the OM2 writer PrometheusProperties props = PrometheusProperties.builder() .openMetrics2Properties( - OpenMetrics2Properties.builder().exemplarCompliance(true).build()) - .build(); - ExpositionFormats formats = ExpositionFormats.init(props); - ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); - // When exemplarCompliance is enabled, should return OM2 writer - assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); - } - - @Test - void testOM2EnabledWithNativeHistograms() { - PrometheusProperties props = - PrometheusProperties.builder() - .openMetrics2Properties(OpenMetrics2Properties.builder().nativeHistograms(true).build()) + OpenMetrics2Properties.builder().contentNegotiation(true).build()) .build(); ExpositionFormats formats = ExpositionFormats.init(props); ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); - // When nativeHistograms is enabled, should return OM2 writer - assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); + assertThat(writer).isInstanceOf(OpenMetricsTextFormatWriter.class); } @Test - void testOM2EnabledWithMultipleFlags() { + void testOM2EnabledWithFeatureFlags() { PrometheusProperties props = PrometheusProperties.builder() .openMetrics2Properties( OpenMetrics2Properties.builder() + .enabled(true) .contentNegotiation(true) .compositeValues(true) .nativeHistograms(true) @@ -180,7 +156,6 @@ void testOM2EnabledWithMultipleFlags() { .build(); ExpositionFormats formats = ExpositionFormats.init(props); ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); - // When multiple OM2 flags are enabled, should return OM2 writer assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); } @@ -188,8 +163,7 @@ void testOM2EnabledWithMultipleFlags() { void testProtobufWriterTakesPrecedence() { PrometheusProperties props = PrometheusProperties.builder() - .openMetrics2Properties( - OpenMetrics2Properties.builder().contentNegotiation(true).build()) + .openMetrics2Properties(OpenMetrics2Properties.builder().enabled(true).build()) .build(); ExpositionFormats formats = ExpositionFormats.init(props); ExpositionFormatWriter writer = diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java index 25f3d9b3d..780f69708 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java @@ -87,7 +87,7 @@ void testGetOpenMetrics2Properties() { } @Test - void testOutputIdenticalToOM1ForCounter() throws IOException { + void testCounterNoTotalSuffix() throws IOException { MetricSnapshots snapshots = MetricSnapshots.of( CounterSnapshot.builder() @@ -101,10 +101,37 @@ void testOutputIdenticalToOM1ForCounter() throws IOException { .build()) .build()); - String om1Output = writeWithOM1(snapshots); String om2Output = writeWithOM2(snapshots); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: name as provided, no _total appending + assertThat(om2Output) + .isEqualTo( + "# TYPE my_counter_seconds counter\n" + + "# UNIT my_counter_seconds seconds\n" + + "# HELP my_counter_seconds Test counter\n" + + "my_counter_seconds{method=\"GET\"} 42.0\n" + + "# EOF\n"); + } + + @Test + void testCounterWithTotalSuffix() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + CounterSnapshot.builder() + .name("requests_total") + .help("Total requests") + .dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder().value(100.0).build()) + .build()); + + String om2Output = writeWithOM2(snapshots); + + // OM2: preserves _total if user provided it + assertThat(om2Output) + .isEqualTo( + "# TYPE requests_total counter\n" + + "# HELP requests_total Total requests\n" + + "requests_total 100.0\n" + + "# EOF\n"); } @Test @@ -220,7 +247,7 @@ void testOutputIdenticalToOM1ForStateSet() throws IOException { } @Test - void testOutputIdenticalToOM1WithExemplars() throws IOException { + void testCounterWithExemplars() throws IOException { Exemplar exemplar = Exemplar.builder() .value(100.0) @@ -241,14 +268,20 @@ void testOutputIdenticalToOM1WithExemplars() throws IOException { .build()) .build()); - String om1Output = writeWithOM1(snapshots); String om2Output = writeWithOM2(snapshots); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: no _total, but exemplar is preserved + assertThat(om2Output) + .isEqualTo( + "# TYPE requests counter\n" + + "# HELP requests Total requests\n" + + "requests 1000.0 # {span_id=\"12345\",trace_id=\"abcde\"}" + + " 100.0 1672850685.829\n" + + "# EOF\n"); } @Test - void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException { + void testCounterWithCreatedTimestamps() throws IOException { MetricSnapshots snapshots = MetricSnapshots.of( CounterSnapshot.builder() @@ -261,16 +294,19 @@ void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException { .build()) .build()); - OpenMetricsTextFormatWriter om1Writer = - OpenMetricsTextFormatWriter.builder().setCreatedTimestampsEnabled(true).build(); - OpenMetrics2TextFormatWriter om2Writer = OpenMetrics2TextFormatWriter.builder().setCreatedTimestampsEnabled(true).build(); - String om1Output = write(snapshots, om1Writer); String om2Output = write(snapshots, om2Writer); - assertThat(om2Output).isEqualTo(om1Output); + // OM2: no _total, _created uses the counter name directly + assertThat(om2Output) + .isEqualTo( + "# TYPE my_counter counter\n" + + "# HELP my_counter Test counter\n" + + "my_counter 42.0\n" + + "my_counter_created 1672850385.800\n" + + "# EOF\n"); } @Test diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java index beca6001e..1d1e7d232 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java @@ -34,7 +34,8 @@ default MetricSnapshot collect(PrometheusScrapeRequest scrapeRequest) { @Nullable default MetricSnapshot collect(Predicate includedNames) { MetricSnapshot result = collect(); - if (includedNames.test(result.getMetadata().getPrometheusName())) { + if (includedNames.test(result.getMetadata().getPrometheusName()) + || includedNames.test(result.getMetadata().getExpositionBasePrometheusName())) { return result; } else { return null; @@ -51,7 +52,8 @@ default MetricSnapshot collect(Predicate includedNames) { default MetricSnapshot collect( Predicate includedNames, PrometheusScrapeRequest scrapeRequest) { MetricSnapshot result = collect(scrapeRequest); - if (includedNames.test(result.getMetadata().getPrometheusName())) { + if (includedNames.test(result.getMetadata().getPrometheusName()) + || includedNames.test(result.getMetadata().getExpositionBasePrometheusName())) { return result; } else { return null; diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java index f66824972..499dbf9d1 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java @@ -28,13 +28,24 @@ public class PrometheusRegistry { private final ConcurrentHashMap> multiCollectorMetadata = new ConcurrentHashMap<>(); + /** + * Maps exposition-level names (e.g. "events_total", "events_created") to the owning + * prometheusName (e.g. "events"). Used to detect cross-type collisions at registration time. + */ + private final ConcurrentHashMap expositionNameOwners = new ConcurrentHashMap<>(); + /** Stores the registration details for a Collector at registration time. */ private static class CollectorRegistration { final String prometheusName; + @Nullable final String expositionBasePrometheusName; final Set labelNames; - CollectorRegistration(String prometheusName, @Nullable Set labelNames) { + CollectorRegistration( + String prometheusName, + @Nullable String expositionBasePrometheusName, + @Nullable Set labelNames) { this.prometheusName = prometheusName; + this.expositionBasePrometheusName = expositionBasePrometheusName; this.labelNames = immutableLabelNames(labelNames); } } @@ -147,9 +158,44 @@ private static Set immutableLabelNames(@Nullable Set labelNames) return Collections.unmodifiableSet(new HashSet<>(labelNames)); } + /** + * Computes the set of exposition-level time series names that a metric with the given name and + * type will produce. + */ + static Set computeExpositionNames(String prometheusName, MetricType type) { + Set names = new HashSet<>(); + switch (type) { + case COUNTER: + names.add(prometheusName + "_total"); + names.add(prometheusName + "_created"); + break; + case INFO: + names.add(prometheusName + "_info"); + break; + case HISTOGRAM: + names.add(prometheusName + "_bucket"); + names.add(prometheusName + "_count"); + names.add(prometheusName + "_sum"); + names.add(prometheusName + "_created"); + break; + case SUMMARY: + names.add(prometheusName + "_count"); + names.add(prometheusName + "_sum"); + names.add(prometheusName + "_created"); + break; + case GAUGE: + case STATESET: + case UNKNOWN: + default: + names.add(prometheusName); + break; + } + return names; + } + /** * Validates the registration of a metric with the given parameters. Ensures type consistency, - * label schema uniqueness, and help/unit consistency. + * label schema uniqueness, help/unit consistency, and exposition name collision detection. */ private void validateRegistration( String prometheusName, @@ -161,6 +207,23 @@ private void validateRegistration( final Set names = normalizedLabels; final String helpForValidation = help; final Unit unitForValidation = unit; + + // Check exposition name collisions before modifying any state. + Set expositionNames = computeExpositionNames(prometheusName, type); + for (String expositionName : expositionNames) { + String owner = expositionNameOwners.get(expositionName); + if (owner != null && !owner.equals(prometheusName)) { + throw new IllegalArgumentException( + "'" + + prometheusName + + "' and '" + + owner + + "' have conflicting exposition name: '" + + expositionName + + "'"); + } + } + registered.compute( prometheusName, (n, existingInfo) -> { @@ -190,6 +253,11 @@ private void validateRegistration( return existingInfo; } }); + + // Registration succeeded — claim exposition names. + for (String expositionName : expositionNames) { + expositionNameOwners.put(expositionName, prometheusName); + } } public void register(Collector collector) { @@ -208,8 +276,12 @@ public void register(Collector collector) { // Collectors that don't implement getPrometheusName()/getMetricType() will skip validation. if (prometheusName != null && metricType != null) { validateRegistration(prometheusName, metricType, normalizedLabels, help, unit); + String expositionBasePrometheusName = + metadata != null ? metadata.getExpositionBasePrometheusName() : null; collectorMetadata.put( - collector, new CollectorRegistration(prometheusName, normalizedLabels)); + collector, + new CollectorRegistration( + prometheusName, expositionBasePrometheusName, normalizedLabels)); } // Catch RuntimeException broadly because collector methods (getPrometheusName, getMetricType, // etc.) are user-implemented and could throw any RuntimeException. Ensures cleanup on @@ -280,7 +352,7 @@ public void unregister(MultiCollector collector) { /** * Removes the label schema for the given metric name. If no label schemas remain for that name, - * removes the metric name entirely from the registry. + * removes the metric name entirely from the registry, including its exposition name reservations. */ private void unregisterLabelSchema(String prometheusName, Set labelNames) { registered.computeIfPresent( @@ -288,6 +360,11 @@ private void unregisterLabelSchema(String prometheusName, Set labelNames (name, info) -> { info.removeLabelSet(labelNames); if (info.isEmpty()) { + // Remove exposition name reservations for this metric. + Set expositionNames = computeExpositionNames(prometheusName, info.getType()); + for (String expositionName : expositionNames) { + expositionNameOwners.remove(expositionName, prometheusName); + } return null; } return info; @@ -300,6 +377,7 @@ public void clear() { registered.clear(); collectorMetadata.clear(); multiCollectorMetadata.clear(); + expositionNameOwners.clear(); } public MetricSnapshots scrape() { @@ -347,7 +425,12 @@ public MetricSnapshots scrape( String prometheusName = collector.getPrometheusName(); // prometheusName == null means the name is unknown, and we have to scrape to learn the name. // prometheusName != null means we can skip the scrape if the name is excluded. - if (prometheusName == null || includedNames.test(prometheusName)) { + // Also test the original name (e.g. "events_total" for a counter named "events"). + CollectorRegistration reg = collectorMetadata.get(collector); + String expositionName = reg != null ? reg.expositionBasePrometheusName : null; + if (prometheusName == null + || includedNames.test(prometheusName) + || (expositionName != null && includedNames.test(expositionName))) { MetricSnapshot snapshot = scrapeRequest == null ? collector.collect(includedNames) diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadata.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadata.java index 9c54f96d5..74f6f55d6 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadata.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadata.java @@ -29,6 +29,22 @@ public final class MetricMetadata { */ private final String prometheusName; + /** + * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, + * for {@code Counter.builder().name("events_total").unit(BYTES)}, this is "events_total_bytes". + * Used by format writers for smart-append logic (e.g. deciding whether to append _total). + */ + private final String expositionBaseName; + + private final String expositionBasePrometheusName; + + /** + * The original name as provided by the user, before any modification (no suffix stripping, no + * unit appending). For example, for {@code Counter.builder().name("req").unit(BYTES)}, this is + * "req". Used by the OTel exporter with {@code preserve_names=true}. + */ + private final String originalName; + @Nullable private final String help; @Nullable private final Unit unit; @@ -52,11 +68,48 @@ public MetricMetadata(String name, String help) { * @param unit optional. May be {@code null}. */ public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) { + this(name, name, help, unit); + } + + /** + * Constructor with exposition base name. + * + * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named + * "events_total") + * @param expositionBaseName the name with unit suffix ensured and type suffix preserved, used by + * format writers for smart-append logic + * @param help optional. May be {@code null}. + * @param unit optional. May be {@code null}. + */ + public MetricMetadata( + String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) { + this(name, expositionBaseName, expositionBaseName, help, unit); + } + + /** + * Constructor with exposition base name and original name. + * + * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named + * "events_total") + * @param expositionBaseName the name with unit suffix ensured and type suffix preserved + * @param originalName the raw name as provided by the user, before any modification + * @param help optional. May be {@code null}. + * @param unit optional. May be {@code null}. + */ + public MetricMetadata( + String name, + String expositionBaseName, + String originalName, + @Nullable String help, + @Nullable Unit unit) { this.name = name; + this.expositionBaseName = expositionBaseName; + this.originalName = originalName; this.help = help; this.unit = unit; validate(); this.prometheusName = PrometheusNaming.prometheusName(name); + this.expositionBasePrometheusName = PrometheusNaming.prometheusName(expositionBaseName); } /** @@ -79,6 +132,32 @@ public String getPrometheusName() { return prometheusName; } + /** + * The original name as provided by the user, before any modification. For example, if the user + * called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link + * #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes". + */ + public String getOriginalName() { + return originalName; + } + + /** + * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, + * if the user called {@code Counter.builder().name("events_total")}, this returns "events_total" + * while {@link #getName()} returns "events". + */ + public String getExpositionBaseName() { + return expositionBaseName; + } + + /** + * Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by + * underscores. + */ + public String getExpositionBasePrometheusName() { + return expositionBasePrometheusName; + } + @Nullable public String getHelp() { return help; @@ -125,6 +204,11 @@ private void validate() { } MetricMetadata escape(EscapingScheme escapingScheme) { - return new MetricMetadata(PrometheusNaming.escapeName(name, escapingScheme), help, unit); + return new MetricMetadata( + PrometheusNaming.escapeName(name, escapingScheme), + PrometheusNaming.escapeName(expositionBaseName, escapingScheme), + PrometheusNaming.escapeName(originalName, escapingScheme), + help, + unit); } } diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java index 4f766fdad..ea2653931 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java @@ -18,34 +18,12 @@ public class PrometheusNaming { /** - * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) - * should also be reserved metric name suffixes. However, popular instrumentation libraries have - * Gauges with names ending in {@code _count}. Examples: + * Test if a metric name is valid. Any non-empty valid UTF-8 string is accepted. * - *
    - *
  • Micrometer: {@code jvm_buffer_count} - *
  • OpenTelemetry: {@code process_runtime_jvm_buffer_count} - *
- * - *

We do not treat {@code _count} and {@code _sum} as reserved suffixes here for compatibility - * with these libraries. However, there is a risk of name conflict if someone creates a gauge - * named {@code my_data_count} and a histogram or summary named {@code my_data}, because the - * histogram or summary will implicitly have a sample named {@code my_data_count}. - */ - private static final String[] RESERVED_METRIC_NAME_SUFFIXES = { - "_total", "_created", "_bucket", "_info", - ".total", ".created", ".bucket", ".info" - }; - - /** - * Test if a metric name is valid. Rules: - * - *

    - *
  • The name must match Metric - * names. - *
  • The name MUST NOT end with one of the {@link #RESERVED_METRIC_NAME_SUFFIXES}. - *
+ *

Collision detection for suffixes like {@code _total}, {@code _info}, {@code _bucket}, etc. + * is handled at registration time by the {@link + * io.prometheus.metrics.model.registry.PrometheusRegistry PrometheusRegistry}, not by name + * validation. * *

If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. Note * that OpenMetrics requires metric names to have their unit @@ -70,11 +48,6 @@ public static boolean isValidMetricName(String name) { */ @Nullable public static String validateMetricName(String name) { - for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { - if (name.endsWith(reservedSuffix)) { - return "The metric name must not include the '" + reservedSuffix + "' suffix."; - } - } if (isValidUtf8(name)) { return null; } @@ -141,10 +114,7 @@ public static boolean isValidLegacyLabelName(String name) { return true; } - /** - * Units may not have illegal characters, and they may not end with a reserved suffix like - * 'total'. - */ + /** Units may not have illegal characters. */ public static boolean isValidUnitName(String name) { return validateUnitName(name) == null; } @@ -155,12 +125,6 @@ public static String validateUnitName(String name) { if (name.isEmpty()) { return "The unit name must not be empty."; } - for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { - String suffixName = reservedSuffix.substring(1); - if (name.endsWith(suffixName)) { - return suffixName + " is a reserved suffix in Prometheus"; - } - } // Check if all characters are [a-zA-Z0-9_.:]+ for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); @@ -189,31 +153,16 @@ public static String prometheusName(String name) { } /** - * Convert an arbitrary string to a name where {@link #isValidMetricName(String) - * isValidMetricName(name)} is true. + * Convert an arbitrary string to a valid metric name. Since any non-empty valid UTF-8 string is a + * valid metric name, this simply returns the input unchanged. + * + * @throws IllegalArgumentException if the input is empty */ public static String sanitizeMetricName(String metricName) { if (metricName.isEmpty()) { throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name."); } - String sanitizedName = metricName; - boolean modified = true; - while (modified) { - modified = false; - for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { - if (sanitizedName.equals(reservedSuffix)) { - // This is for the corner case when you call sanitizeMetricName("_total"). - // In that case the result will be "total". - return reservedSuffix.substring(1); - } - if (sanitizedName.endsWith(reservedSuffix)) { - sanitizedName = - sanitizedName.substring(0, sanitizedName.length() - reservedSuffix.length()); - modified = true; - } - } - } - return sanitizedName; + return metricName; } /** @@ -249,11 +198,10 @@ public static String sanitizeLabelName(String labelName) { } /** - * Convert an arbitrary string to a name where {@link #validateUnitName(String)} is {@code null} - * (i.e. the name is valid). + * Convert an arbitrary string to a valid unit name by replacing illegal characters. * - * @throws IllegalArgumentException if the {@code unitName} cannot be converted, for example if - * you call {@code sanitizeUnitName("total")} or {@code sanitizeUnitName("")}. + * @throws IllegalArgumentException if the {@code unitName} cannot be converted, e.g. if you call + * {@code sanitizeUnitName("")}. * @throws NullPointerException if {@code unitName} is null. */ public static String sanitizeUnitName(String unitName) { @@ -261,24 +209,11 @@ public static String sanitizeUnitName(String unitName) { throw new IllegalArgumentException("Cannot convert an empty string to a valid unit name."); } String sanitizedName = replaceIllegalCharsInUnitName(unitName); - boolean modified = true; - while (modified) { - modified = false; - while (sanitizedName.startsWith("_") || sanitizedName.startsWith(".")) { - sanitizedName = sanitizedName.substring(1); - modified = true; - } - while (sanitizedName.endsWith(".") || sanitizedName.endsWith("_")) { - sanitizedName = sanitizedName.substring(0, sanitizedName.length() - 1); - modified = true; - } - for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { - String suffixName = reservedSuffix.substring(1); - if (sanitizedName.endsWith(suffixName)) { - sanitizedName = sanitizedName.substring(0, sanitizedName.length() - suffixName.length()); - modified = true; - } - } + while (sanitizedName.startsWith("_") || sanitizedName.startsWith(".")) { + sanitizedName = sanitizedName.substring(1); + } + while (sanitizedName.endsWith(".") || sanitizedName.endsWith("_")) { + sanitizedName = sanitizedName.substring(0, sanitizedName.length() - 1); } if (sanitizedName.isEmpty()) { throw new IllegalArgumentException( diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java index 422b36ee0..b4f69e9bb 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/SnapshotEscaper.java @@ -96,6 +96,15 @@ public static String getMetadataName(MetricMetadata metadata, EscapingScheme sch } } + public static String getExpositionBaseMetadataName( + MetricMetadata metadata, EscapingScheme scheme) { + if (scheme == EscapingScheme.UNDERSCORE_ESCAPING) { + return metadata.getExpositionBasePrometheusName(); + } else { + return metadata.getExpositionBaseName(); + } + } + public static Labels escapeLabels(Labels labels, EscapingScheme scheme) { Labels.Builder outLabelsBuilder = Labels.builder(); diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java index 90a04934e..6dabad653 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java @@ -1087,4 +1087,193 @@ public String getPrometheusName() { assertThatCode(() -> registry.unregister(legacy)).doesNotThrowAnyException(); assertThat(registry.scrape().size()).isEqualTo(0); } + + @Test + void register_gaugeWithTotalSuffix_andCounter_collisionDetected() { + PrometheusRegistry registry = new PrometheusRegistry(); + + // Register a counter "events" — this claims exposition name "events_total" + Collector counter = + new Collector() { + @Override + public MetricSnapshot collect() { + return CounterSnapshot.builder().name("events").build(); + } + + @Override + public String getPrometheusName() { + return "events"; + } + + @Override + public MetricType getMetricType() { + return MetricType.COUNTER; + } + }; + + // Register a gauge "events_total" — this claims exposition name "events_total" + Collector gauge = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("events_total").build(); + } + + @Override + public String getPrometheusName() { + return "events_total"; + } + + @Override + public MetricType getMetricType() { + return MetricType.GAUGE; + } + }; + + registry.register(counter); + assertThatThrownBy(() -> registry.register(gauge)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("conflicting exposition name"); + } + + @Test + void register_gaugeWithCountSuffix_andHistogram_collisionDetected() { + PrometheusRegistry registry = new PrometheusRegistry(); + + Collector histogram = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("foo").build(); + } + + @Override + public String getPrometheusName() { + return "foo"; + } + + @Override + public MetricType getMetricType() { + return MetricType.HISTOGRAM; + } + }; + + Collector gauge = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("foo_count").build(); + } + + @Override + public String getPrometheusName() { + return "foo_count"; + } + + @Override + public MetricType getMetricType() { + return MetricType.GAUGE; + } + }; + + registry.register(histogram); + assertThatThrownBy(() -> registry.register(gauge)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("conflicting exposition name"); + } + + @Test + void register_gaugeWithTotalSuffix_andHistogram_noCollision() { + PrometheusRegistry registry = new PrometheusRegistry(); + + // Histogram "foo" claims: foo_bucket, foo_count, foo_sum, foo_created + // Gauge "foo_total" claims: foo_total + // No overlap — should succeed. + Collector histogram = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("foo").build(); + } + + @Override + public String getPrometheusName() { + return "foo"; + } + + @Override + public MetricType getMetricType() { + return MetricType.HISTOGRAM; + } + }; + + Collector gauge = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("foo_total").build(); + } + + @Override + public String getPrometheusName() { + return "foo_total"; + } + + @Override + public MetricType getMetricType() { + return MetricType.GAUGE; + } + }; + + registry.register(histogram); + assertThatCode(() -> registry.register(gauge)).doesNotThrowAnyException(); + } + + @Test + void register_expositionCollision_unregisterAndReregister() { + PrometheusRegistry registry = new PrometheusRegistry(); + + Collector counter = + new Collector() { + @Override + public MetricSnapshot collect() { + return CounterSnapshot.builder().name("events").build(); + } + + @Override + public String getPrometheusName() { + return "events"; + } + + @Override + public MetricType getMetricType() { + return MetricType.COUNTER; + } + }; + + Collector gauge = + new Collector() { + @Override + public MetricSnapshot collect() { + return GaugeSnapshot.builder().name("events_total").build(); + } + + @Override + public String getPrometheusName() { + return "events_total"; + } + + @Override + public MetricType getMetricType() { + return MetricType.GAUGE; + } + }; + + registry.register(counter); + assertThatThrownBy(() -> registry.register(gauge)).isInstanceOf(IllegalArgumentException.class); + + // After unregistering the counter, the gauge should succeed + registry.unregister(counter); + assertThatCode(() -> registry.register(gauge)).doesNotThrowAnyException(); + } } diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/CounterSnapshotTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/CounterSnapshotTest.java index ca4346cdb..16a324323 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/CounterSnapshotTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/CounterSnapshotTest.java @@ -92,8 +92,8 @@ void testEmptyCounter() { @Test void testTotalSuffixPresent() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> CounterSnapshot.builder().name("test_total").build()); + CounterSnapshot snapshot = CounterSnapshot.builder().name("test_total").build(); + assertThat(snapshot.getMetadata().getPrometheusName()).isEqualTo("test_total"); } @Test diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/GaugeSnapshotTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/GaugeSnapshotTest.java index 5154e1eb1..7bd965913 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/GaugeSnapshotTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/GaugeSnapshotTest.java @@ -87,14 +87,14 @@ void testEmptyGauge() { @Test void testTotalSuffixPresent() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> CounterSnapshot.builder().name("test_total").build()); + CounterSnapshot snapshot = CounterSnapshot.builder().name("test_total").build(); + assertThat(snapshot.getMetadata().getPrometheusName()).isEqualTo("test_total"); } @Test void testTotalSuffixPresentDot() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> CounterSnapshot.builder().name("test.total").build()); + CounterSnapshot snapshot = CounterSnapshot.builder().name("test.total").build(); + assertThat(snapshot.getMetadata().getPrometheusName()).isEqualTo("test_total"); } @Test diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/InfoSnapshotTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/InfoSnapshotTest.java index 3cf7d69af..608876bdf 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/InfoSnapshotTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/InfoSnapshotTest.java @@ -61,13 +61,13 @@ void testDataImmutable() { @Test void testNameMustNotIncludeSuffix() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> InfoSnapshot.builder().name("jvm_info").build()); + InfoSnapshot snapshot = InfoSnapshot.builder().name("jvm_info").build(); + assertThat(snapshot.getMetadata().getPrometheusName()).isEqualTo("jvm_info"); } @Test void testNameMustNotIncludeSuffixDot() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> InfoSnapshot.builder().name("jvm.info").build()); + InfoSnapshot snapshot = InfoSnapshot.builder().name("jvm.info").build(); + assertThat(snapshot.getMetadata().getPrometheusName()).isEqualTo("jvm_info"); } } diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/MetricMetadataTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/MetricMetadataTest.java index 41efe043b..8a4731ac8 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/MetricMetadataTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/MetricMetadataTest.java @@ -34,21 +34,27 @@ void testSanitizationIllegalCharacters() { } @Test - void testSanitizationCounter() { + void testNameWithTotalSuffix() { MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_events_total")); - assertThat(metadata.getName()).isEqualTo("my_events"); + assertThat(metadata.getName()).isEqualTo("my_events_total"); } @Test - void testSanitizationInfo() { + void testNameWithInfoSuffix() { MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("target_info")); - assertThat(metadata.getName()).isEqualTo("target"); + assertThat(metadata.getName()).isEqualTo("target_info"); } @Test - void testSanitizationWeirdCornerCase() { - MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("_total_created")); - assertThat(metadata.getName()).isEqualTo("total"); + void testNameWithCreatedSuffix() { + MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_events_created")); + assertThat(metadata.getName()).isEqualTo("my_events_created"); + } + + @Test + void testNameWithBucketSuffix() { + MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_histogram_bucket")); + assertThat(metadata.getName()).isEqualTo("my_histogram_bucket"); } @Test @@ -72,4 +78,29 @@ void testUnitSuffixAdded() { void testUnitNotDuplicated() { assertThat(sanitizeMetricName("my_counter_bytes", Unit.BYTES)).isEqualTo("my_counter_bytes"); } + + @Test + void testFiveArgConstructor() { + MetricMetadata metadata = + new MetricMetadata("req_bytes", "req_bytes", "req", "help", Unit.BYTES); + assertThat(metadata.getName()).isEqualTo("req_bytes"); + assertThat(metadata.getExpositionBaseName()).isEqualTo("req_bytes"); + assertThat(metadata.getOriginalName()).isEqualTo("req"); + assertThat(metadata.getHelp()).isEqualTo("help"); + assertThat(metadata.getUnit()).isEqualTo(Unit.BYTES); + } + + @Test + void testFourArgConstructorDefaultsOriginalName() { + MetricMetadata metadata = new MetricMetadata("req_bytes", "req_bytes", "help", Unit.BYTES); + assertThat(metadata.getOriginalName()).isEqualTo("req_bytes"); + assertThat(metadata.getExpositionBaseName()).isEqualTo("req_bytes"); + } + + @Test + void testThreeArgConstructorDefaultsOriginalName() { + MetricMetadata metadata = new MetricMetadata("req_bytes", "help", Unit.BYTES); + assertThat(metadata.getOriginalName()).isEqualTo("req_bytes"); + assertThat(metadata.getExpositionBaseName()).isEqualTo("req_bytes"); + } } diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java index 847bb0f38..dcebd14d8 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java @@ -22,13 +22,14 @@ class PrometheusNamingTest { @Test void testSanitizeMetricName() { - assertThat(sanitizeMetricName("my_counter_total")).isEqualTo("my_counter"); - assertThat(sanitizeMetricName("jvm.info")).isEqualTo("jvm"); - assertThat(sanitizeMetricName("jvm_info")).isEqualTo("jvm"); - assertThat(sanitizeMetricName("jvm.info")).isEqualTo("jvm"); + assertThat(sanitizeMetricName("my_counter_total")).isEqualTo("my_counter_total"); + assertThat(sanitizeMetricName("jvm.info")).isEqualTo("jvm.info"); + assertThat(sanitizeMetricName("jvm_info")).isEqualTo("jvm_info"); assertThat(sanitizeMetricName("a.b")).isEqualTo("a.b"); - assertThat(sanitizeMetricName("_total")).isEqualTo("total"); + assertThat(sanitizeMetricName("_total")).isEqualTo("_total"); assertThat(sanitizeMetricName("total")).isEqualTo("total"); + assertThat(sanitizeMetricName("my_events_created")).isEqualTo("my_events_created"); + assertThat(sanitizeMetricName("my_histogram_bucket")).isEqualTo("my_histogram_bucket"); } @Test @@ -36,9 +37,9 @@ void testSanitizeMetricNameWithUnit() { assertThat(prometheusName(sanitizeMetricName("def", Unit.RATIO))) .isEqualTo("def_" + Unit.RATIO); assertThat(prometheusName(sanitizeMetricName("my_counter_total", Unit.RATIO))) - .isEqualTo("my_counter_" + Unit.RATIO); - assertThat(sanitizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm_" + Unit.RATIO); - assertThat(sanitizeMetricName("_total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO); + .isEqualTo("my_counter_total_" + Unit.RATIO); + assertThat(sanitizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm.info_" + Unit.RATIO); + assertThat(sanitizeMetricName("_total", Unit.RATIO)).isEqualTo("_total_" + Unit.RATIO); assertThat(sanitizeMetricName("total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO); } @@ -55,40 +56,29 @@ void testSanitizeLabelName() { @Test void testValidateUnitName() { - assertThat(validateUnitName("secondstotal")).isNotNull(); - assertThat(validateUnitName("total")).isNotNull(); - assertThat(validateUnitName("seconds_total")).isNotNull(); - assertThat(validateUnitName("_total")).isNotNull(); assertThat(validateUnitName("")).isNotNull(); assertThat(validateUnitName("seconds")).isNull(); assertThat(validateUnitName("2")).isNull(); + assertThat(validateUnitName("total")).isNull(); + assertThat(validateUnitName("info")).isNull(); + assertThat(validateUnitName("created")).isNull(); + assertThat(validateUnitName("bucket")).isNull(); } @Test void testSanitizeUnitName() { assertThat(sanitizeUnitName("seconds")).isEqualTo("seconds"); - assertThat(sanitizeUnitName("seconds_total")).isEqualTo("seconds"); - assertThat(sanitizeUnitName("seconds_total_total")).isEqualTo("seconds"); assertThat(sanitizeUnitName("m/s")).isEqualTo("m_s"); - assertThat(sanitizeUnitName("secondstotal")).isEqualTo("seconds"); assertThat(sanitizeUnitName("2")).isEqualTo("2"); + assertThat(sanitizeUnitName("total")).isEqualTo("total"); + assertThat(sanitizeUnitName("info")).isEqualTo("info"); + assertThat(sanitizeUnitName("created")).isEqualTo("created"); + assertThat(sanitizeUnitName("bucket")).isEqualTo("bucket"); } @Test - void testInvalidUnitName1() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> sanitizeUnitName("total")); - } - - @Test - void testInvalidUnitName2() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> sanitizeUnitName("_total")); - } - - @Test - void testInvalidUnitName3() { + void testInvalidUnitName_percent() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> sanitizeUnitName("%")); }