From ec60b586375f74a1fe8ad6e4b62f70f24e25643a Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Fri, 6 Mar 2026 15:22:18 -0500 Subject: [PATCH] Add OpenMetrics2 configuration support Signed-off-by: Jay DeLuca --- .../config/OpenMetrics2Properties.java | 142 ++++++++++++++++++ .../metrics/config/PrometheusProperties.java | 27 +++- .../config/PrometheusPropertiesLoader.java | 4 +- .../config/OpenMetrics2PropertiesTest.java | 137 +++++++++++++++++ .../config/PrometheusPropertiesTest.java | 67 +++++++++ 5 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java create mode 100644 prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java 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 new file mode 100644 index 000000000..e356c4e65 --- /dev/null +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java @@ -0,0 +1,142 @@ +package io.prometheus.metrics.config; + +import javax.annotation.Nullable; + +/** Properties starting with io.prometheus.open_metrics2 */ +public class OpenMetrics2Properties { + + private static final String PREFIX = "io.prometheus.open_metrics2"; + private static final String CONTENT_NEGOTIATION = "content_negotiation"; + private static final String DISABLE_SUFFIX_APPENDING = "disable_suffix_appending"; + 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 contentNegotiation; + @Nullable private final Boolean disableSuffixAppending; + @Nullable private final Boolean compositeValues; + @Nullable private final Boolean exemplarCompliance; + @Nullable private final Boolean nativeHistograms; + + private OpenMetrics2Properties( + @Nullable Boolean contentNegotiation, + @Nullable Boolean disableSuffixAppending, + @Nullable Boolean compositeValues, + @Nullable Boolean exemplarCompliance, + @Nullable Boolean nativeHistograms) { + this.contentNegotiation = contentNegotiation; + this.disableSuffixAppending = disableSuffixAppending; + this.compositeValues = compositeValues; + this.exemplarCompliance = exemplarCompliance; + this.nativeHistograms = nativeHistograms; + } + + /** Gate OM2 features behind content negotiation. Default is {@code false}. */ + public boolean getContentNegotiation() { + return contentNegotiation != null && contentNegotiation; + } + + /** Suppress {@code _total} and unit suffixes. Default is {@code false}. */ + public boolean getDisableSuffixAppending() { + return disableSuffixAppending != null && disableSuffixAppending; + } + + /** Single-line histogram/summary with {@code st@}. Default is {@code false}. */ + public boolean getCompositeValues() { + return compositeValues != null && compositeValues; + } + + /** Mandatory timestamps, no 128-char limit for exemplars. Default is {@code false}. */ + public boolean getExemplarCompliance() { + return exemplarCompliance != null && exemplarCompliance; + } + + /** Exponential buckets support for native histograms. Default is {@code false}. */ + public boolean getNativeHistograms() { + return nativeHistograms != null && nativeHistograms; + } + + /** + * 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. + */ + static OpenMetrics2Properties load(PropertySource propertySource) + throws PrometheusPropertiesException { + Boolean contentNegotiation = Util.loadBoolean(PREFIX, CONTENT_NEGOTIATION, propertySource); + Boolean disableSuffixAppending = + Util.loadBoolean(PREFIX, DISABLE_SUFFIX_APPENDING, 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, + disableSuffixAppending, + compositeValues, + exemplarCompliance, + nativeHistograms); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + @Nullable private Boolean contentNegotiation; + @Nullable private Boolean disableSuffixAppending; + @Nullable private Boolean compositeValues; + @Nullable private Boolean exemplarCompliance; + @Nullable private Boolean nativeHistograms; + + private Builder() {} + + /** See {@link #getContentNegotiation()} */ + public Builder contentNegotiation(boolean contentNegotiation) { + this.contentNegotiation = contentNegotiation; + return this; + } + + /** See {@link #getDisableSuffixAppending()} */ + public Builder disableSuffixAppending(boolean disableSuffixAppending) { + this.disableSuffixAppending = disableSuffixAppending; + return this; + } + + /** See {@link #getCompositeValues()} */ + public Builder compositeValues(boolean compositeValues) { + this.compositeValues = compositeValues; + return this; + } + + /** See {@link #getExemplarCompliance()} */ + public Builder exemplarCompliance(boolean exemplarCompliance) { + this.exemplarCompliance = exemplarCompliance; + return this; + } + + /** See {@link #getNativeHistograms()} */ + public Builder nativeHistograms(boolean nativeHistograms) { + this.nativeHistograms = nativeHistograms; + return this; + } + + /** Enable all OpenMetrics 2.0 features */ + public Builder enableAll() { + this.contentNegotiation = true; + this.disableSuffixAppending = true; + this.compositeValues = true; + this.exemplarCompliance = true; + this.nativeHistograms = true; + return this; + } + + public OpenMetrics2Properties build() { + return new OpenMetrics2Properties( + contentNegotiation, + disableSuffixAppending, + 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 055fe4aa3..a9045d711 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 @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import javax.annotation.Nullable; /** @@ -21,6 +22,7 @@ public class PrometheusProperties { private final ExporterHttpServerProperties exporterHttpServerProperties; private final ExporterOpenTelemetryProperties exporterOpenTelemetryProperties; private final ExporterPushgatewayProperties exporterPushgatewayProperties; + private final OpenMetrics2Properties openMetrics2Properties; /** * Map that stores metric-specific properties keyed by metric name in exposition format @@ -111,7 +113,8 @@ public static Builder builder() { ExporterFilterProperties exporterFilterProperties, ExporterHttpServerProperties httpServerConfig, ExporterPushgatewayProperties pushgatewayProperties, - ExporterOpenTelemetryProperties otelConfig) { + ExporterOpenTelemetryProperties otelConfig, + OpenMetrics2Properties openMetrics2Properties) { this.defaultMetricsProperties = defaultMetricsProperties; this.metricProperties = metricProperties; this.exemplarProperties = exemplarProperties; @@ -120,6 +123,7 @@ public static Builder builder() { this.exporterHttpServerProperties = httpServerConfig; this.exporterPushgatewayProperties = pushgatewayProperties; this.exporterOpenTelemetryProperties = otelConfig; + this.openMetrics2Properties = openMetrics2Properties; } /** @@ -167,6 +171,10 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } + public OpenMetrics2Properties getOpenMetrics2Properties() { + return openMetrics2Properties; + } + public static class Builder { private MetricsProperties defaultMetricsProperties = MetricsProperties.builder().build(); private final MetricPropertiesMap metricProperties = new MetricPropertiesMap(); @@ -180,6 +188,8 @@ public static class Builder { ExporterPushgatewayProperties.builder().build(); private ExporterOpenTelemetryProperties otelConfig = ExporterOpenTelemetryProperties.builder().build(); + private OpenMetrics2Properties openMetrics2Properties = + OpenMetrics2Properties.builder().build(); private Builder() {} @@ -231,6 +241,18 @@ public Builder exporterOpenTelemetryProperties( return this; } + public Builder enableOpenMetrics2(Consumer configurator) { + OpenMetrics2Properties.Builder openMetrics2Builder = OpenMetrics2Properties.builder(); + configurator.accept(openMetrics2Builder); + this.openMetrics2Properties = openMetrics2Builder.build(); + return this; + } + + public Builder openMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) { + this.openMetrics2Properties = openMetrics2Properties; + return this; + } + public PrometheusProperties build() { return new PrometheusProperties( defaultMetricsProperties, @@ -240,7 +262,8 @@ public PrometheusProperties build() { exporterFilterProperties, exporterHttpServerProperties, pushgatewayProperties, - otelConfig); + otelConfig, + openMetrics2Properties); } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java index 9119c2f65..018ea2c5d 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java @@ -40,6 +40,7 @@ public static PrometheusProperties load(Map externalProperties) ExporterPushgatewayProperties.load(propertySource); ExporterOpenTelemetryProperties exporterOpenTelemetryProperties = ExporterOpenTelemetryProperties.load(propertySource); + OpenMetrics2Properties openMetrics2Properties = OpenMetrics2Properties.load(propertySource); validateAllPropertiesProcessed(propertySource); return new PrometheusProperties( defaultMetricsProperties, @@ -49,7 +50,8 @@ public static PrometheusProperties load(Map externalProperties) exporterFilterProperties, exporterHttpServerProperties, exporterPushgatewayProperties, - exporterOpenTelemetryProperties); + exporterOpenTelemetryProperties, + openMetrics2Properties); } // This will remove entries from propertySource when they are processed. 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 new file mode 100644 index 000000000..3ad833df5 --- /dev/null +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java @@ -0,0 +1,137 @@ +package io.prometheus.metrics.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class OpenMetrics2PropertiesTest { + + @Test + void load() { + OpenMetrics2Properties properties = + load( + new HashMap<>( + Map.of( + "io.prometheus.open_metrics2.content_negotiation", + "true", + "io.prometheus.open_metrics2.disable_suffix_appending", + "true", + "io.prometheus.open_metrics2.composite_values", + "true", + "io.prometheus.open_metrics2.exemplar_compliance", + "true", + "io.prometheus.open_metrics2.native_histograms", + "true"))); + assertThat(properties.getContentNegotiation()).isTrue(); + assertThat(properties.getDisableSuffixAppending()).isTrue(); + assertThat(properties.getCompositeValues()).isTrue(); + assertThat(properties.getExemplarCompliance()).isTrue(); + assertThat(properties.getNativeHistograms()).isTrue(); + } + + @Test + void loadInvalidValue() { + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.open_metrics2.content_negotiation", "invalid")))) + .withMessage( + "io.prometheus.open_metrics2.content_negotiation: Expecting 'true' or 'false'. Found:" + + " invalid"); + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.open_metrics2.disable_suffix_appending", "invalid")))) + .withMessage( + "io.prometheus.open_metrics2.disable_suffix_appending: Expecting 'true' or 'false'." + + " Found: invalid"); + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.open_metrics2.composite_values", "invalid")))) + .withMessage( + "io.prometheus.open_metrics2.composite_values: Expecting 'true' or 'false'. Found:" + + " invalid"); + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.open_metrics2.exemplar_compliance", "invalid")))) + .withMessage( + "io.prometheus.open_metrics2.exemplar_compliance: Expecting 'true' or 'false'. Found:" + + " invalid"); + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.open_metrics2.native_histograms", "invalid")))) + .withMessage( + "io.prometheus.open_metrics2.native_histograms: Expecting 'true' or 'false'. Found:" + + " invalid"); + } + + private static OpenMetrics2Properties load(Map map) { + Map regularProperties = new HashMap<>(map); + PropertySource propertySource = new PropertySource(regularProperties); + return OpenMetrics2Properties.load(propertySource); + } + + @Test + void builder() { + OpenMetrics2Properties properties = + OpenMetrics2Properties.builder() + .contentNegotiation(true) + .disableSuffixAppending(true) + .compositeValues(false) + .exemplarCompliance(true) + .nativeHistograms(false) + .build(); + assertThat(properties.getContentNegotiation()).isTrue(); + assertThat(properties.getDisableSuffixAppending()).isTrue(); + assertThat(properties.getCompositeValues()).isFalse(); + assertThat(properties.getExemplarCompliance()).isTrue(); + assertThat(properties.getNativeHistograms()).isFalse(); + } + + @Test + void builderEnableAll() { + OpenMetrics2Properties properties = OpenMetrics2Properties.builder().enableAll().build(); + assertThat(properties.getContentNegotiation()).isTrue(); + assertThat(properties.getDisableSuffixAppending()).isTrue(); + assertThat(properties.getCompositeValues()).isTrue(); + assertThat(properties.getExemplarCompliance()).isTrue(); + assertThat(properties.getNativeHistograms()).isTrue(); + } + + @Test + void defaultValues() { + OpenMetrics2Properties properties = OpenMetrics2Properties.builder().build(); + assertThat(properties.getContentNegotiation()).isFalse(); + assertThat(properties.getDisableSuffixAppending()).isFalse(); + assertThat(properties.getCompositeValues()).isFalse(); + assertThat(properties.getExemplarCompliance()).isFalse(); + assertThat(properties.getNativeHistograms()).isFalse(); + } + + @Test + void partialConfiguration() { + OpenMetrics2Properties properties = + OpenMetrics2Properties.builder().contentNegotiation(true).compositeValues(true).build(); + assertThat(properties.getContentNegotiation()).isTrue(); + assertThat(properties.getDisableSuffixAppending()).isFalse(); + assertThat(properties.getCompositeValues()).isTrue(); + assertThat(properties.getExemplarCompliance()).isFalse(); + assertThat(properties.getNativeHistograms()).isFalse(); + } +} diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 3e891202a..633afb781 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -128,4 +128,71 @@ void testMetricNameStartingWithNumber() { assertThat(result.getMetricProperties("123metric")).isSameAs(customProps); assertThat(result.getMetricProperties("_23metric")).isSameAs(customProps); } + + @Test + void testOpenMetrics2ConsumerPattern() { + PrometheusProperties config = + PrometheusProperties.builder() + .enableOpenMetrics2( + om2 -> + om2.contentNegotiation(true) + .disableSuffixAppending(true) + .compositeValues(false)) + .build(); + assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getDisableSuffixAppending()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isFalse(); + } + + @Test + void testOpenMetrics2EnableAll() { + PrometheusProperties config = + PrometheusProperties.builder().enableOpenMetrics2(om2 -> om2.enableAll()).build(); + assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getDisableSuffixAppending()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isTrue(); + } + + @Test + void testOpenMetrics2DirectAssignment() { + OpenMetrics2Properties om2Props = + OpenMetrics2Properties.builder().contentNegotiation(true).nativeHistograms(true).build(); + PrometheusProperties config = + PrometheusProperties.builder().openMetrics2Properties(om2Props).build(); + assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getDisableSuffixAppending()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isTrue(); + } + + @Test + void testOpenMetrics2Defaults() { + PrometheusProperties config = PrometheusProperties.builder().build(); + assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getDisableSuffixAppending()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isFalse(); + } + + @Test + void testOpenMetrics2PropertiesLoading() { + Map properties = new HashMap<>(); + properties.put("io.prometheus.open_metrics2.content_negotiation", "true"); + properties.put("io.prometheus.open_metrics2.disable_suffix_appending", "true"); + properties.put("io.prometheus.open_metrics2.composite_values", "false"); + properties.put("io.prometheus.open_metrics2.exemplar_compliance", "true"); + properties.put("io.prometheus.open_metrics2.native_histograms", "false"); + PrometheusProperties config = PrometheusPropertiesLoader.load(properties); + assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getDisableSuffixAppending()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse(); + assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isTrue(); + assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isFalse(); + } }