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
This commit is contained in:
parent
c81a0223cc
commit
11b4a19dee
|
@ -16,6 +16,15 @@ The resulting response is similar to the following:
|
||||||
|
|
||||||
include::{snippets}/prometheus/all/http-response.adoc[]
|
include::{snippets}/prometheus/all/http-response.adoc[]
|
||||||
|
|
||||||
|
The default response content type is `text/plain;version=0.0.4`.
|
||||||
|
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:
|
||||||
|
|
||||||
|
include::{snippets}/prometheus/openmetrics/curl-request.adoc[]
|
||||||
|
|
||||||
|
The resulting response is similar to the following:
|
||||||
|
|
||||||
|
include::{snippets}/prometheus/openmetrics/http-response.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[prometheus-retrieving-query-parameters]]
|
[[prometheus-retrieving-query-parameters]]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -20,6 +20,7 @@ import io.micrometer.core.instrument.Clock;
|
||||||
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
|
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
|
||||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||||
import io.prometheus.client.CollectorRegistry;
|
import io.prometheus.client.CollectorRegistry;
|
||||||
|
import io.prometheus.client.exporter.common.TextFormat;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
|
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
|
||||||
|
@ -31,6 +32,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.docu
|
||||||
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
|
||||||
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
|
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,6 +48,14 @@ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocument
|
||||||
this.mockMvc.perform(get("/actuator/prometheus")).andExpect(status().isOk()).andDo(document("prometheus/all"));
|
this.mockMvc.perform(get("/actuator/prometheus")).andExpect(status().isOk()).andDo(document("prometheus/all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prometheusOpenmetrics() throws Exception {
|
||||||
|
this.mockMvc.perform(get("/actuator/prometheus").accept(TextFormat.CONTENT_TYPE_OPENMETRICS_100))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(header().string("Content-Type", "application/openmetrics-text;version=1.0.0;charset=utf-8"))
|
||||||
|
.andDo(document("prometheus/openmetrics"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void filteredPrometheus() throws Exception {
|
void filteredPrometheus() throws Exception {
|
||||||
this.mockMvc
|
this.mockMvc
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.metrics.export.prometheus;
|
||||||
|
|
||||||
|
import io.prometheus.client.exporter.common.TextFormat;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.endpoint.http.Producible;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Producible} for Prometheus's {@link TextFormat}.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @since 2.5.0
|
||||||
|
*/
|
||||||
|
public enum ProducibleTextFormat implements Producible<ProducibleTextFormat> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Openmetrics text version 1.0.0.
|
||||||
|
*/
|
||||||
|
CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prometheus text version 0.0.4.
|
||||||
|
*/
|
||||||
|
CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004);
|
||||||
|
|
||||||
|
private final MimeType mimeType;
|
||||||
|
|
||||||
|
ProducibleTextFormat(String mimeType) {
|
||||||
|
this.mimeType = MimeTypeUtils.parseMimeType(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MimeType getMimeType() {
|
||||||
|
return this.mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -28,8 +28,10 @@ import io.prometheus.client.exporter.common.TextFormat;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
|
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Endpoint @Endpoint} that outputs metrics in a format that can be scraped by the
|
* {@link Endpoint @Endpoint} that outputs metrics in a format that can be scraped by the
|
||||||
|
@ -48,15 +50,25 @@ public class PrometheusScrapeEndpoint {
|
||||||
this.collectorRegistry = collectorRegistry;
|
this.collectorRegistry = collectorRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReadOperation(produces = TextFormat.CONTENT_TYPE_004)
|
@ReadOperation(produces = { TextFormat.CONTENT_TYPE_004, TextFormat.CONTENT_TYPE_OPENMETRICS_100 })
|
||||||
public String scrape(@Nullable Set<String> includedNames) {
|
public WebEndpointResponse<String> scrape(ProducibleTextFormat producibleTextFormat,
|
||||||
|
@Nullable Set<String> includedNames) {
|
||||||
try {
|
try {
|
||||||
Writer writer = new StringWriter();
|
Writer writer = new StringWriter();
|
||||||
Enumeration<MetricFamilySamples> samples = (includedNames != null)
|
Enumeration<MetricFamilySamples> samples = (includedNames != null)
|
||||||
? this.collectorRegistry.filteredMetricFamilySamples(includedNames)
|
? this.collectorRegistry.filteredMetricFamilySamples(includedNames)
|
||||||
: this.collectorRegistry.metricFamilySamples();
|
: this.collectorRegistry.metricFamilySamples();
|
||||||
TextFormat.write004(writer, samples);
|
MimeType contentType = producibleTextFormat.getMimeType();
|
||||||
return writer.toString();
|
if (producibleTextFormat == ProducibleTextFormat.CONTENT_TYPE_004) {
|
||||||
|
TextFormat.write004(writer, samples);
|
||||||
|
}
|
||||||
|
else if (producibleTextFormat == ProducibleTextFormat.CONTENT_TYPE_OPENMETRICS_100) {
|
||||||
|
TextFormat.writeOpenMetrics100(writer, samples);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new RuntimeException("Unsupported text format '" + producibleTextFormat.getMimeType() + "'");
|
||||||
|
}
|
||||||
|
return new WebEndpointResponse<>(writer.toString(), contentType);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
// This actually never happens since StringWriter::write() doesn't throw any
|
// This actually never happens since StringWriter::write() doesn't throw any
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2021 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -40,13 +40,21 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
class PrometheusScrapeEndpointIntegrationTests {
|
class PrometheusScrapeEndpointIntegrationTests {
|
||||||
|
|
||||||
@WebEndpointTest
|
@WebEndpointTest
|
||||||
void scrapeHasContentTypeText004(WebTestClient client) {
|
void scrapeHasContentTypeText004ByDefault(WebTestClient client) {
|
||||||
client.get().uri("/actuator/prometheus").exchange().expectStatus().isOk().expectHeader()
|
client.get().uri("/actuator/prometheus").exchange().expectStatus().isOk().expectHeader()
|
||||||
.contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)).expectBody(String.class)
|
.contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)).expectBody(String.class)
|
||||||
.value((body) -> assertThat(body).contains("counter1_total").contains("counter2_total")
|
.value((body) -> assertThat(body).contains("counter1_total").contains("counter2_total")
|
||||||
.contains("counter3_total"));
|
.contains("counter3_total"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WebEndpointTest
|
||||||
|
void scrapeCanProduceOpenMetrics100(WebTestClient client) {
|
||||||
|
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
|
||||||
|
client.get().uri("/actuator/prometheus").accept(openMetrics).exchange().expectStatus().isOk().expectHeader()
|
||||||
|
.contentType(openMetrics).expectBody(String.class).value((body) -> assertThat(body)
|
||||||
|
.contains("counter1_total").contains("counter2_total").contains("counter3_total"));
|
||||||
|
}
|
||||||
|
|
||||||
@WebEndpointTest
|
@WebEndpointTest
|
||||||
void scrapeWithIncludedNames(WebTestClient client) {
|
void scrapeWithIncludedNames(WebTestClient client) {
|
||||||
client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus()
|
client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus()
|
||||||
|
|
Loading…
Reference in New Issue