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 a4a7088b8..3863b5b74 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 @@ -63,9 +63,17 @@ public ExpositionFormatWriter findWriter(@Nullable String acceptHeader) { return prometheusProtobufWriter; } - // Prefer OM2 over OM1 when any OM2 feature is enabled if (isOpenMetrics2Enabled() && openMetrics2TextFormatWriter.accepts(acceptHeader)) { - return openMetrics2TextFormatWriter; + if (openMetrics2TextFormatWriter.getOpenMetrics2Properties().getContentNegotiation()) { + String version = parseOpenMetricsVersion(acceptHeader); + if ("2.0.0".equals(version)) { + return openMetrics2TextFormatWriter; + } + // version=1.0.0 or no version: fall through to OM1 + } else { + // contentNegotiation=false: OM2 handles all OpenMetrics requests + return openMetrics2TextFormatWriter; + } } if (openMetricsTextFormatWriter.accepts(acceptHeader)) { @@ -94,4 +102,27 @@ public OpenMetricsTextFormatWriter getOpenMetricsTextFormatWriter() { public OpenMetrics2TextFormatWriter getOpenMetrics2TextFormatWriter() { return openMetrics2TextFormatWriter; } + + @Nullable + private static String parseOpenMetricsVersion(@Nullable String acceptHeader) { + if (acceptHeader == null) { + return null; + } + for (String mediaType : acceptHeader.split(",")) { + if (mediaType.contains("application/openmetrics-text")) { + for (String param : mediaType.split(";")) { + String[] tokens = param.split("="); + if (tokens.length == 2) { + String key = tokens[0].trim(); + String value = tokens[1].trim(); + if (key.equals("version")) { + return value; + } + } + } + return null; + } + } + return null; + } } 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 339c5dfa0..e0782251e 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 @@ -155,7 +155,8 @@ void testOM2EnabledWithFeatureFlags() { .build()) .build(); ExpositionFormats formats = ExpositionFormats.init(props); - ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); + ExpositionFormatWriter writer = + formats.findWriter("application/openmetrics-text; version=2.0.0"); assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); } @@ -173,6 +174,94 @@ void testProtobufWriterTakesPrecedence() { assertThat(writer).isInstanceOf(PrometheusProtobufWriter.class); } + @Test + void testOM2ContentNegotiationWithVersion2() { + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(true).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = + formats.findWriter("application/openmetrics-text; version=2.0.0"); + assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); + } + + @Test + void testOM2ContentNegotiationWithVersion1() { + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(true).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = + formats.findWriter("application/openmetrics-text; version=1.0.0"); + assertThat(writer).isInstanceOf(OpenMetricsTextFormatWriter.class); + } + + @Test + void testOM2ContentNegotiationWithNoVersion() { + // When contentNegotiation=true and Accept header has no version, use OM1 writer + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(true).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = formats.findWriter("application/openmetrics-text"); + assertThat(writer).isInstanceOf(OpenMetricsTextFormatWriter.class); + } + + @Test + void testOM2ContentNegotiationDisabled() { + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(false).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer1 = formats.findWriter("application/openmetrics-text"); + ExpositionFormatWriter writer2 = + formats.findWriter("application/openmetrics-text; version=1.0.0"); + ExpositionFormatWriter writer3 = + formats.findWriter("application/openmetrics-text; version=2.0.0"); + assertThat(writer1).isInstanceOf(OpenMetrics2TextFormatWriter.class); + assertThat(writer2).isInstanceOf(OpenMetrics2TextFormatWriter.class); + assertThat(writer3).isInstanceOf(OpenMetrics2TextFormatWriter.class); + } + + @Test + void testOM2ContentNegotiationMultiTypeAcceptHeaderWithoutOMVersion() { + // When the Accept header has multiple media types and only text/plain has a version, + // the openmetrics type should be treated as having no version (use OM1). + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(true).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = + formats.findWriter("application/openmetrics-text, text/plain; version=0.0.4"); + assertThat(writer).isInstanceOf(OpenMetricsTextFormatWriter.class); + } + + @Test + void testOM2ContentNegotiationMultiTypeAcceptHeaderWithOMVersion() { + // When the Accept header has multiple media types and openmetrics has version=2.0.0, + // the OM2 writer should be selected. + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder().enabled(true).contentNegotiation(true).build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = + formats.findWriter( + "application/openmetrics-text; version=2.0.0, text/plain; version=0.0.4"); + assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); + } + @Test void testCounterComplete() throws IOException { String openMetricsText = @@ -1977,14 +2066,14 @@ void testClassicGaugeHistogramCountAndSum() throws IOException { void testClassicHistogramWithDots() throws IOException { String openMetricsText = """ - # TYPE U__my_2e_request_2e_duration_2e_seconds histogram - # UNIT U__my_2e_request_2e_duration_2e_seconds seconds - # HELP U__my_2e_request_2e_duration_2e_seconds Request duration in seconds - U__my_2e_request_2e_duration_2e_seconds_bucket{U__http_2e_path="/hello",le="+Inf"} 130 # {U__some_2e_exemplar_2e_key="some value"} 3.0 1690298864.383 - U__my_2e_request_2e_duration_2e_seconds_count{U__http_2e_path="/hello"} 130 - U__my_2e_request_2e_duration_2e_seconds_sum{U__http_2e_path="/hello"} 0.01 - # EOF - """; + # TYPE U__my_2e_request_2e_duration_2e_seconds histogram + # UNIT U__my_2e_request_2e_duration_2e_seconds seconds + # HELP U__my_2e_request_2e_duration_2e_seconds Request duration in seconds + U__my_2e_request_2e_duration_2e_seconds_bucket{U__http_2e_path="/hello",le="+Inf"} 130 # {U__some_2e_exemplar_2e_key="some value"} 3.0 1690298864.383 + U__my_2e_request_2e_duration_2e_seconds_count{U__http_2e_path="/hello"} 130 + U__my_2e_request_2e_duration_2e_seconds_sum{U__http_2e_path="/hello"} 0.01 + # EOF + """; String openMetricsTextWithExemplarsOnAllTimeSeries = "# TYPE my_request_duration_seconds histogram\n" + "# UNIT my_request_duration_seconds seconds\n" @@ -2398,14 +2487,14 @@ void testNativeHistogramMinimal() throws IOException { void testNativeHistogramWithDots() throws IOException { String openMetricsText = """ - # TYPE U__my_2e_request_2e_duration_2e_seconds histogram - # UNIT U__my_2e_request_2e_duration_2e_seconds seconds - # HELP U__my_2e_request_2e_duration_2e_seconds Request duration in seconds - U__my_2e_request_2e_duration_2e_seconds_bucket{U__http_2e_path="/hello",le="+Inf"} 4 # {U__some_2e_exemplar_2e_key="some value"} 3.0 1690298864.383 - U__my_2e_request_2e_duration_2e_seconds_count{U__http_2e_path="/hello"} 4 - U__my_2e_request_2e_duration_2e_seconds_sum{U__http_2e_path="/hello"} 3.2 - # EOF - """; + # TYPE U__my_2e_request_2e_duration_2e_seconds histogram + # UNIT U__my_2e_request_2e_duration_2e_seconds seconds + # HELP U__my_2e_request_2e_duration_2e_seconds Request duration in seconds + U__my_2e_request_2e_duration_2e_seconds_bucket{U__http_2e_path="/hello",le="+Inf"} 4 # {U__some_2e_exemplar_2e_key="some value"} 3.0 1690298864.383 + U__my_2e_request_2e_duration_2e_seconds_count{U__http_2e_path="/hello"} 4 + U__my_2e_request_2e_duration_2e_seconds_sum{U__http_2e_path="/hello"} 3.2 + # EOF + """; String openMetricsTextWithExemplarsOnAllTimeSeries = "# TYPE my_request_duration_seconds histogram\n" + "# UNIT my_request_duration_seconds seconds\n" @@ -2876,12 +2965,19 @@ void testLabelValueEscape() throws IOException { @ParameterizedTest @CsvSource({ - "'application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited', 'application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores'", + "'application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited'," + + " 'application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;" + + " encoding=delimited; escaping=underscores'", "'text/plain;version=0.0.4', 'text/plain; version=0.0.4; charset=utf-8; escaping=underscores'", - "'application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited; escaping=allow-utf-8', 'application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8'", - "'application/openmetrics-text', 'application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores'", - "'application/openmetrics-text;version=0.0.1; escaping=underscores', 'application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores'", - "'text/plain;version=0.0.4; escaping=allow-utf-8', 'text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8'" + "'application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;" + + " escaping=allow-utf-8', 'application/vnd.google.protobuf;" + + " proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8'", + "'application/openmetrics-text', 'application/openmetrics-text; version=1.0.0; charset=utf-8;" + + " escaping=underscores'", + "'application/openmetrics-text;version=0.0.1; escaping=underscores'," + + " 'application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores'", + "'text/plain;version=0.0.4; escaping=allow-utf-8', 'text/plain; version=0.0.4; charset=utf-8;" + + " escaping=allow-utf-8'" }) public void testFindWriter(String acceptHeaderValue, String expectedFmt) { ExpositionFormats expositionFormats = ExpositionFormats.init(); @@ -2910,9 +3006,9 @@ void testWrite() throws IOException { String expected = """ - # TYPE foo_metric untyped - foo_metric 1.234 - """; + # TYPE foo_metric untyped + foo_metric 1.234 + """; assertThat(new String(out, UTF_8)).hasToString(expected); }