Skip to content

Commit 11b4a19

Browse files
wilkinsonaphilwebb
authored andcommitted
Support OpenMetrics text format with Prometheus
Update `PrometheusScrapeEndpoint` so that it can produce both classic Prometheus text output as well as Openmetrics output. See gh-25564
1 parent c81a022 commit 11b4a19

File tree

5 files changed

+101
-8
lines changed

5 files changed

+101
-8
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/prometheus.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ The resulting response is similar to the following:
1616

1717
include::{snippets}/prometheus/all/http-response.adoc[]
1818

19+
The default response content type is `text/plain;version=0.0.4`.
20+
The endpoint can also produce `application/openmetrics-text;version=1.0.0` when called with an appropriate `Accept` header, as shown in the following curl-based example:
21+
22+
include::{snippets}/prometheus/openmetrics/curl-request.adoc[]
23+
24+
The resulting response is similar to the following:
25+
26+
include::{snippets}/prometheus/openmetrics/http-response.adoc[]
27+
1928

2029

2130
[[prometheus-retrieving-query-parameters]]

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
2121
import io.micrometer.prometheus.PrometheusMeterRegistry;
2222
import io.prometheus.client.CollectorRegistry;
23+
import io.prometheus.client.exporter.common.TextFormat;
2324
import org.junit.jupiter.api.Test;
2425

2526
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
@@ -31,6 +32,7 @@
3132
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
3233
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
3334
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
35+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
3436
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
3537

3638
/**
@@ -46,6 +48,14 @@ void prometheus() throws Exception {
4648
this.mockMvc.perform(get("/actuator/prometheus")).andExpect(status().isOk()).andDo(document("prometheus/all"));
4749
}
4850

51+
@Test
52+
void prometheusOpenmetrics() throws Exception {
53+
this.mockMvc.perform(get("/actuator/prometheus").accept(TextFormat.CONTENT_TYPE_OPENMETRICS_100))
54+
.andExpect(status().isOk())
55+
.andExpect(header().string("Content-Type", "application/openmetrics-text;version=1.0.0;charset=utf-8"))
56+
.andDo(document("prometheus/openmetrics"));
57+
}
58+
4959
@Test
5060
void filteredPrometheus() throws Exception {
5161
this.mockMvc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.metrics.export.prometheus;
18+
19+
import io.prometheus.client.exporter.common.TextFormat;
20+
21+
import org.springframework.boot.actuate.endpoint.http.Producible;
22+
import org.springframework.util.MimeType;
23+
import org.springframework.util.MimeTypeUtils;
24+
25+
/**
26+
* A {@link Producible} for Prometheus's {@link TextFormat}.
27+
*
28+
* @author Andy Wilkinson
29+
* @since 2.5.0
30+
*/
31+
public enum ProducibleTextFormat implements Producible<ProducibleTextFormat> {
32+
33+
/**
34+
* Openmetrics text version 1.0.0.
35+
*/
36+
CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100),
37+
38+
/**
39+
* Prometheus text version 0.0.4.
40+
*/
41+
CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004);
42+
43+
private final MimeType mimeType;
44+
45+
ProducibleTextFormat(String mimeType) {
46+
this.mimeType = MimeTypeUtils.parseMimeType(mimeType);
47+
}
48+
49+
@Override
50+
public MimeType getMimeType() {
51+
return this.mimeType;
52+
}
53+
54+
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,8 +28,10 @@
2828

2929
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3030
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
31+
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
3132
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
3233
import org.springframework.lang.Nullable;
34+
import org.springframework.util.MimeType;
3335

3436
/**
3537
* {@link Endpoint @Endpoint} that outputs metrics in a format that can be scraped by the
@@ -48,15 +50,25 @@ public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) {
4850
this.collectorRegistry = collectorRegistry;
4951
}
5052

51-
@ReadOperation(produces = TextFormat.CONTENT_TYPE_004)
52-
public String scrape(@Nullable Set<String> includedNames) {
53+
@ReadOperation(produces = { TextFormat.CONTENT_TYPE_004, TextFormat.CONTENT_TYPE_OPENMETRICS_100 })
54+
public WebEndpointResponse<String> scrape(ProducibleTextFormat producibleTextFormat,
55+
@Nullable Set<String> includedNames) {
5356
try {
5457
Writer writer = new StringWriter();
5558
Enumeration<MetricFamilySamples> samples = (includedNames != null)
5659
? this.collectorRegistry.filteredMetricFamilySamples(includedNames)
5760
: this.collectorRegistry.metricFamilySamples();
58-
TextFormat.write004(writer, samples);
59-
return writer.toString();
61+
MimeType contentType = producibleTextFormat.getMimeType();
62+
if (producibleTextFormat == ProducibleTextFormat.CONTENT_TYPE_004) {
63+
TextFormat.write004(writer, samples);
64+
}
65+
else if (producibleTextFormat == ProducibleTextFormat.CONTENT_TYPE_OPENMETRICS_100) {
66+
TextFormat.writeOpenMetrics100(writer, samples);
67+
}
68+
else {
69+
throw new RuntimeException("Unsupported text format '" + producibleTextFormat.getMimeType() + "'");
70+
}
71+
return new WebEndpointResponse<>(writer.toString(), contentType);
6072
}
6173
catch (IOException ex) {
6274
// This actually never happens since StringWriter::write() doesn't throw any

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,13 +40,21 @@
4040
class PrometheusScrapeEndpointIntegrationTests {
4141

4242
@WebEndpointTest
43-
void scrapeHasContentTypeText004(WebTestClient client) {
43+
void scrapeHasContentTypeText004ByDefault(WebTestClient client) {
4444
client.get().uri("/actuator/prometheus").exchange().expectStatus().isOk().expectHeader()
4545
.contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)).expectBody(String.class)
4646
.value((body) -> assertThat(body).contains("counter1_total").contains("counter2_total")
4747
.contains("counter3_total"));
4848
}
4949

50+
@WebEndpointTest
51+
void scrapeCanProduceOpenMetrics100(WebTestClient client) {
52+
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
53+
client.get().uri("/actuator/prometheus").accept(openMetrics).exchange().expectStatus().isOk().expectHeader()
54+
.contentType(openMetrics).expectBody(String.class).value((body) -> assertThat(body)
55+
.contains("counter1_total").contains("counter2_total").contains("counter3_total"));
56+
}
57+
5058
@WebEndpointTest
5159
void scrapeWithIncludedNames(WebTestClient client) {
5260
client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus()

0 commit comments

Comments
 (0)