Browse Source

Support Pushgateway with new Prometheus client

Closes gh-43923
pull/44069/head
Andy Wilkinson 11 months ago
parent
commit
63ecfac40d
  1. 4
      spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle
  2. 79
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java
  3. 79
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java
  4. 8
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
  5. 6
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java
  6. 128
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java
  7. 4
      spring-boot-project/spring-boot-actuator/build.gradle
  8. 49
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java
  9. 79
      spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java
  10. 5
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc

4
spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

@ -54,9 +54,6 @@ dependencies { @@ -54,9 +54,6 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging"
exclude group: "javax.annotation", module: "javax.annotation-api"
}
optional("io.prometheus:simpleclient_pushgateway") {
exclude group: "javax.xml.bind", module: "jaxb-api"
}
optional("io.micrometer:micrometer-registry-signalfx")
optional("io.micrometer:micrometer-registry-statsd")
optional("io.micrometer:micrometer-registry-wavefront")
@ -65,6 +62,7 @@ dependencies { @@ -65,6 +62,7 @@ dependencies {
optional("io.opentelemetry:opentelemetry-exporter-zipkin")
optional("io.opentelemetry:opentelemetry-exporter-otlp")
optional("io.projectreactor.netty:reactor-netty-http")
optional("io.prometheus:prometheus-metrics-exporter-pushgateway")
optional("io.r2dbc:r2dbc-pool")
optional("io.r2dbc:r2dbc-proxy")
optional("io.r2dbc:r2dbc-spi")

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

@ -19,6 +19,10 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus @@ -19,6 +19,10 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus
import io.micrometer.core.instrument.Clock;
import io.micrometer.prometheusmetrics.PrometheusConfig;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.exporter.pushgateway.Format;
import io.prometheus.metrics.exporter.pushgateway.PushGateway;
import io.prometheus.metrics.exporter.pushgateway.PushGateway.Builder;
import io.prometheus.metrics.exporter.pushgateway.Scheme;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.tracer.common.SpanContext;
@ -28,15 +32,20 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegi @@ -28,15 +32,20 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegi
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to Prometheus.
@ -87,4 +96,74 @@ public class PrometheusMetricsExportAutoConfiguration { @@ -87,4 +96,74 @@ public class PrometheusMetricsExportAutoConfiguration {
}
/**
* Configuration for <a href="https://github.com/prometheus/pushgateway">Prometheus
* Pushgateway</a>.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PushGateway.class)
@ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled")
static class PrometheusPushGatewayConfiguration {
/**
* The fallback job name. We use 'spring' since there's a history of Prometheus
* spring integration defaulting to that name from when Prometheus integration
* didn't exist in Spring itself.
*/
private static final String FALLBACK_JOB = "spring";
@Bean
@ConditionalOnMissingBean
PrometheusPushGatewayManager prometheusPushGatewayManager(PrometheusRegistry registry,
PrometheusProperties prometheusProperties, Environment environment) {
PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway();
PushGateway pushGateway = initializePushGateway(registry, properties, environment);
return new PrometheusPushGatewayManager(pushGateway, properties.getPushRate(),
properties.getShutdownOperation());
}
private PushGateway initializePushGateway(PrometheusRegistry registry,
PrometheusProperties.Pushgateway properties, Environment environment) {
Builder builder = PushGateway.builder()
.address(properties.getAddress())
.scheme(scheme(properties))
.format(format(properties))
.job(getJob(properties, environment))
.registry(registry);
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
entries.put("management.prometheus.metrics.export.pushgateway.token", properties.getToken());
entries.put("management.prometheus.metrics.export.pushgateway.username", properties.getUsername());
});
if (StringUtils.hasText(properties.getToken())) {
builder.bearerToken(properties.getToken());
}
else if (StringUtils.hasText(properties.getUsername())) {
builder.basicAuth(properties.getUsername(), properties.getPassword());
}
properties.getGroupingKey().forEach(builder::groupingKey);
return builder.build();
}
private Scheme scheme(PrometheusProperties.Pushgateway properties) {
return switch (properties.getScheme()) {
case HTTP -> Scheme.HTTP;
case HTTPS -> Scheme.HTTPS;
};
}
private Format format(PrometheusProperties.Pushgateway properties) {
return switch (properties.getFormat()) {
case PROTOBUF -> Format.PROMETHEUS_PROTOBUF;
case TEXT -> Format.PROMETHEUS_TEXT;
};
}
private String getJob(PrometheusProperties.Pushgateway properties, Environment environment) {
String job = properties.getJob();
job = (job != null) ? job : environment.getProperty("spring.application.name");
return (job != null) ? job : FALLBACK_JOB;
}
}
}

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

@ -104,9 +104,14 @@ public class PrometheusProperties { @@ -104,9 +104,14 @@ public class PrometheusProperties {
private Boolean enabled = false;
/**
* Base URL for the Pushgateway.
* Address (host:port) for the Pushgateway.
*/
private String baseUrl = "http://localhost:9091";
private String address = "localhost:9091";
/**
* The scheme to use when pushing metrics.
*/
private Scheme scheme = Scheme.HTTP;
/**
* Login user of the Prometheus Pushgateway.
@ -118,6 +123,16 @@ public class PrometheusProperties { @@ -118,6 +123,16 @@ public class PrometheusProperties {
*/
private String password;
/**
* The token to use for authentication with the Prometheus Pushgateway.
*/
private String token;
/**
* The format to use when pushing metrics.
*/
private Format format = Format.PROTOBUF;
/**
* Frequency with which to push metrics.
*/
@ -146,12 +161,12 @@ public class PrometheusProperties { @@ -146,12 +161,12 @@ public class PrometheusProperties {
this.enabled = enabled;
}
public String getBaseUrl() {
return this.baseUrl;
public String getAddress() {
return this.address;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
public void setAddress(String address) {
this.address = address;
}
public String getUsername() {
@ -202,6 +217,58 @@ public class PrometheusProperties { @@ -202,6 +217,58 @@ public class PrometheusProperties {
this.shutdownOperation = shutdownOperation;
}
public Scheme getScheme() {
return this.scheme;
}
public void setScheme(Scheme scheme) {
this.scheme = scheme;
}
public String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
public Format getFormat() {
return this.format;
}
public void setFormat(Format format) {
this.format = format;
}
public enum Format {
/**
* Push metrics in text format.
*/
TEXT,
/**
* Push metrics in protobuf format.
*/
PROTOBUF
}
public enum Scheme {
/**
* Use HTTP to push metrics.
*/
HTTP,
/**
* Use HTTPS to push metrics.
*/
HTTPS
}
}
}

8
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

@ -2083,6 +2083,14 @@ @@ -2083,6 +2083,14 @@
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of tracing is enabled to export OTLP traces."
},
{
"name": "management.promethus.metrics.export.pushgateway.base-url",
"type": "java.lang.String",
"deprecation": {
"level": "error",
"replacement": "management.prometheus.metrics.export.pushgateway.address"
}
},
{
"name": "management.server.add-application-context-header",
"type": "java.lang.Boolean",

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -21,7 +21,7 @@ import java.util.Properties; @@ -21,7 +21,7 @@ import java.util.Properties;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.client.exporter.common.TextFormat;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import org.junit.jupiter.api.Test;
@ -50,7 +50,7 @@ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocument @@ -50,7 +50,7 @@ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocument
@Test
void prometheusOpenmetrics() {
assertThat(this.mvc.get().uri("/actuator/prometheus").accept(TextFormat.CONTENT_TYPE_OPENMETRICS_100))
assertThat(this.mvc.get().uri("/actuator/prometheus").accept(OpenMetricsTextFormatWriter.CONTENT_TYPE))
.satisfies((result) -> {
assertThat(result).hasStatusOk()
.headers()

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -16,22 +16,38 @@ @@ -16,22 +16,38 @@
package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus;
import java.net.MalformedURLException;
import java.net.URI;
import io.micrometer.core.instrument.Clock;
import io.micrometer.prometheusmetrics.PrometheusConfig;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.exporter.pushgateway.DefaultHttpConnectionFactory;
import io.prometheus.metrics.exporter.pushgateway.HttpConnectionFactory;
import io.prometheus.metrics.exporter.pushgateway.PushGateway;
import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -158,6 +174,116 @@ class PrometheusMetricsExportAutoConfigurationTests { @@ -158,6 +174,116 @@ class PrometheusMetricsExportAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class));
}
@Test
@ExtendWith(OutputCaptureExtension.class)
void withPushGatewayEnabled(CapturedOutput output) {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> {
assertThat(output).doesNotContain("Invalid PushGateway base url");
hasGatewayUrl(context, "http://localhost:9091/metrics/job/spring");
});
}
@Test
void withPushGatewayNoBasicAuth() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true")
.withUserConfiguration(BaseConfiguration.class)
.run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory)
.isInstanceOf(DefaultHttpConnectionFactory.class)));
}
@Test
void withCustomPushGatewayAddress() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.address=localhost:8080")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> hasGatewayUrl(context, "http://localhost:8080/metrics/job/spring"));
}
@Test
void withCustomScheme() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.scheme=https")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> hasGatewayUrl(context, "https://localhost:9091/metrics/job/spring"));
}
@Test
void withCustomFormat() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.format=text")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(getPushGateway(context)).extracting("writer")
.isInstanceOf(PrometheusTextFormatWriter.class));
}
@Test
void withPushGatewayBasicAuth() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.username=admin",
"management.prometheus.metrics.export.pushgateway.password=secret")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(getPushGateway(context))
.extracting("requestHeaders", InstanceOfAssertFactories.map(String.class, String.class))
.satisfies((headers) -> assertThat(headers.get("Authorization")).startsWith("Basic ")));
}
@Test
void withPushGatewayBearerToken() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.token=a1b2c3d4")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(getPushGateway(context))
.extracting("requestHeaders", InstanceOfAssertFactories.map(String.class, String.class))
.satisfies((headers) -> assertThat(headers.get("Authorization")).startsWith("Bearer ")));
}
@Test
void failsFastWithBothBearerAndBasicAuthentication() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.username=alice",
"management.prometheus.metrics.export.pushgateway.token=a1b2c3d4")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(context).getFailure()
.hasRootCauseInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)
.hasMessageContainingAll("management.prometheus.metrics.export.pushgateway.username",
"management.prometheus.metrics.export.pushgateway.token"));
}
private void hasGatewayUrl(AssertableApplicationContext context, String url) {
try {
assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("url", URI.create(url).toURL());
}
catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
private ContextConsumer<AssertableApplicationContext> hasHttpConnectionFactory(
ThrowingConsumer<HttpConnectionFactory> httpConnectionFactory) {
return (context) -> {
PushGateway pushGateway = getPushGateway(context);
httpConnectionFactory
.accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory"));
};
}
private PushGateway getPushGateway(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class);
PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class);
return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway");
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {

4
spring-boot-project/spring-boot-actuator/build.gradle

@ -39,9 +39,7 @@ dependencies { @@ -39,9 +39,7 @@ dependencies {
optional("io.micrometer:micrometer-registry-prometheus")
optional("io.micrometer:micrometer-registry-prometheus-simpleclient")
optional("io.prometheus:prometheus-metrics-exposition-formats")
optional("io.prometheus:simpleclient_pushgateway") {
exclude(group: "javax.xml.bind", module: "jaxb-api")
}
optional("io.prometheus:prometheus-metrics-exporter-pushgateway")
optional("io.r2dbc:r2dbc-pool")
optional("io.r2dbc:r2dbc-spi")
optional("io.undertow:undertow-servlet")

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

@ -17,13 +17,11 @@ @@ -17,13 +17,11 @@
package org.springframework.boot.actuate.metrics.export.prometheus;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.PushGateway;
import io.prometheus.metrics.exporter.pushgateway.PushGateway;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -46,12 +44,6 @@ public class PrometheusPushGatewayManager { @@ -46,12 +44,6 @@ public class PrometheusPushGatewayManager {
private final PushGateway pushGateway;
private final CollectorRegistry registry;
private final String job;
private final Map<String, String> groupingKey;
private final ShutdownOperation shutdownOperation;
private final TaskScheduler scheduler;
@ -59,43 +51,24 @@ public class PrometheusPushGatewayManager { @@ -59,43 +51,24 @@ public class PrometheusPushGatewayManager {
private final ScheduledFuture<?> scheduled;
/**
* Create a new {@link PrometheusPushGatewayManager} instance using a single threaded
* {@link TaskScheduler}.
* Create a new {@link PrometheusPushGatewayManager} instance.
* @param pushGateway the source push gateway
* @param registry the collector registry to push
* @param pushRate the rate at which push operations occur
* @param job the job ID for the operation
* @param groupingKeys an optional set of grouping keys for the operation
* @param shutdownOperation the shutdown operation that should be performed when
* context is closed.
* @since 3.5.0
*/
public PrometheusPushGatewayManager(PushGateway pushGateway, CollectorRegistry registry, Duration pushRate,
String job, Map<String, String> groupingKeys, ShutdownOperation shutdownOperation) {
this(pushGateway, registry, new PushGatewayTaskScheduler(), pushRate, job, groupingKeys, shutdownOperation);
public PrometheusPushGatewayManager(PushGateway pushGateway, Duration pushRate,
ShutdownOperation shutdownOperation) {
this(pushGateway, new PushGatewayTaskScheduler(), pushRate, shutdownOperation);
}
/**
* Create a new {@link PrometheusPushGatewayManager} instance.
* @param pushGateway the source push gateway
* @param registry the collector registry to push
* @param scheduler the scheduler used for operations
* @param pushRate the rate at which push operations occur
* @param job the job ID for the operation
* @param groupingKey an optional set of grouping keys for the operation
* @param shutdownOperation the shutdown operation that should be performed when
* context is closed.
*/
public PrometheusPushGatewayManager(PushGateway pushGateway, CollectorRegistry registry, TaskScheduler scheduler,
Duration pushRate, String job, Map<String, String> groupingKey, ShutdownOperation shutdownOperation) {
PrometheusPushGatewayManager(PushGateway pushGateway, TaskScheduler scheduler, Duration pushRate,
ShutdownOperation shutdownOperation) {
Assert.notNull(pushGateway, "'pushGateway' must not be null");
Assert.notNull(registry, "'registry' must not be null");
Assert.notNull(scheduler, "'scheduler' must not be null");
Assert.notNull(pushRate, "'pushRate' must not be null");
Assert.hasLength(job, "'job' must not be empty");
this.pushGateway = pushGateway;
this.registry = registry;
this.job = job;
this.groupingKey = groupingKey;
this.shutdownOperation = (shutdownOperation != null) ? shutdownOperation : ShutdownOperation.NONE;
this.scheduler = scheduler;
this.scheduled = this.scheduler.scheduleAtFixedRate(this::post, pushRate);
@ -103,7 +76,7 @@ public class PrometheusPushGatewayManager { @@ -103,7 +76,7 @@ public class PrometheusPushGatewayManager {
private void post() {
try {
this.pushGateway.pushAdd(this.registry, this.job, this.groupingKey);
this.pushGateway.pushAdd();
}
catch (Throwable ex) {
logger.warn("Unexpected exception thrown by POST of metrics to Prometheus Pushgateway", ex);
@ -112,7 +85,7 @@ public class PrometheusPushGatewayManager { @@ -112,7 +85,7 @@ public class PrometheusPushGatewayManager {
private void put() {
try {
this.pushGateway.push(this.registry, this.job, this.groupingKey);
this.pushGateway.push();
}
catch (Throwable ex) {
logger.warn("Unexpected exception thrown by PUT of metrics to Prometheus Pushgateway", ex);
@ -121,7 +94,7 @@ public class PrometheusPushGatewayManager { @@ -121,7 +94,7 @@ public class PrometheusPushGatewayManager {
private void delete() {
try {
this.pushGateway.delete(this.job, this.groupingKey);
this.pushGateway.delete();
}
catch (Throwable ex) {
logger.warn("Unexpected exception thrown by DELETE of metrics from Prometheus Pushgateway", ex);

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

@ -17,12 +17,9 @@ @@ -17,12 +17,9 @@
package org.springframework.boot.actuate.metrics.export.prometheus;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.PushGateway;
import io.prometheus.metrics.exporter.pushgateway.PushGateway;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@ -55,16 +52,11 @@ class PrometheusPushGatewayManagerTests { @@ -55,16 +52,11 @@ class PrometheusPushGatewayManagerTests {
@Mock
private PushGateway pushGateway;
@Mock
private CollectorRegistry registry;
@Mock
private TaskScheduler scheduler;
private final Duration pushRate = Duration.ofSeconds(1);
private final Map<String, String> groupingKey = Collections.singletonMap("foo", "bar");
@Captor
private ArgumentCaptor<Runnable> task;
@ -74,68 +66,48 @@ class PrometheusPushGatewayManagerTests { @@ -74,68 +66,48 @@ class PrometheusPushGatewayManagerTests {
@Test
void createWhenPushGatewayIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PrometheusPushGatewayManager(null, this.registry, this.scheduler, this.pushRate,
"job", this.groupingKey, null))
.isThrownBy(() -> new PrometheusPushGatewayManager(null, this.scheduler, this.pushRate, null))
.withMessage("'pushGateway' must not be null");
}
@Test
void createWhenCollectorRegistryIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, null, this.scheduler, this.pushRate,
"job", this.groupingKey, null))
.withMessage("'registry' must not be null");
}
@Test
void createWhenSchedulerIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, this.registry, null, this.pushRate,
"job", this.groupingKey, null))
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, null, this.pushRate, null))
.withMessage("'scheduler' must not be null");
}
@Test
void createWhenPushRateIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, null,
"job", this.groupingKey, null))
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, this.scheduler, null, null))
.withMessage("'pushRate' must not be null");
}
@Test
void createWhenJobIsEmptyThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler,
this.pushRate, "", this.groupingKey, null))
.withMessage("'job' must not be empty");
}
@Test
void createShouldSchedulePushAsFixedRate() throws Exception {
new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job",
this.groupingKey, null);
new PrometheusPushGatewayManager(this.pushGateway, this.scheduler, this.pushRate, null);
then(this.scheduler).should().scheduleAtFixedRate(this.task.capture(), eq(this.pushRate));
this.task.getValue().run();
then(this.pushGateway).should().pushAdd(this.registry, "job", this.groupingKey);
then(this.pushGateway).should().pushAdd();
}
@Test
void shutdownWhenOwnsSchedulerDoesShutdownScheduler() {
void shutdownWhenOwnsSchedulerDoesShutDownScheduler() {
PushGatewayTaskScheduler ownedScheduler = givenScheduleAtFixedRateWillReturnFuture(
mock(PushGatewayTaskScheduler.class));
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
ownedScheduler, this.pushRate, "job", this.groupingKey, null);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, ownedScheduler,
this.pushRate, null);
manager.shutdown();
then(ownedScheduler).should().shutdown();
}
@Test
void shutdownWhenDoesNotOwnSchedulerDoesNotShutdownScheduler() {
void shutdownWhenDoesNotOwnSchedulerDoesNotShutDownScheduler() {
ThreadPoolTaskScheduler otherScheduler = givenScheduleAtFixedRateWillReturnFuture(
mock(ThreadPoolTaskScheduler.class));
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
otherScheduler, this.pushRate, "job", this.groupingKey, null);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, otherScheduler,
this.pushRate, null);
manager.shutdown();
then(otherScheduler).should(never()).shutdown();
}
@ -143,38 +115,38 @@ class PrometheusPushGatewayManagerTests { @@ -143,38 +115,38 @@ class PrometheusPushGatewayManagerTests {
@Test
void shutdownWhenShutdownOperationIsPostPerformsPushAddOnShutdown() throws Exception {
givenScheduleAtFixedRateWithReturnFuture();
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.POST);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.scheduler,
this.pushRate, ShutdownOperation.POST);
manager.shutdown();
then(this.future).should().cancel(false);
then(this.pushGateway).should().pushAdd(this.registry, "job", this.groupingKey);
then(this.pushGateway).should().pushAdd();
}
@Test
void shutdownWhenShutdownOperationIsPutPerformsPushOnShutdown() throws Exception {
givenScheduleAtFixedRateWithReturnFuture();
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.PUT);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.scheduler,
this.pushRate, ShutdownOperation.PUT);
manager.shutdown();
then(this.future).should().cancel(false);
then(this.pushGateway).should().push(this.registry, "job", this.groupingKey);
then(this.pushGateway).should().push();
}
@Test
void shutdownWhenShutdownOperationIsDeletePerformsDeleteOnShutdown() throws Exception {
givenScheduleAtFixedRateWithReturnFuture();
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.DELETE);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.scheduler,
this.pushRate, ShutdownOperation.DELETE);
manager.shutdown();
then(this.future).should().cancel(false);
then(this.pushGateway).should().delete("job", this.groupingKey);
then(this.pushGateway).should().delete();
}
@Test
void shutdownWhenShutdownOperationIsNoneDoesNothing() {
givenScheduleAtFixedRateWithReturnFuture();
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.NONE);
PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.scheduler,
this.pushRate, ShutdownOperation.NONE);
manager.shutdown();
then(this.future).should().cancel(false);
then(this.pushGateway).shouldHaveNoInteractions();
@ -182,10 +154,9 @@ class PrometheusPushGatewayManagerTests { @@ -182,10 +154,9 @@ class PrometheusPushGatewayManagerTests {
@Test
void pushDoesNotThrowException() throws Exception {
new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job",
this.groupingKey, null);
new PrometheusPushGatewayManager(this.pushGateway, this.scheduler, this.pushRate, null);
then(this.scheduler).should().scheduleAtFixedRate(this.task.capture(), eq(this.pushRate));
willThrow(RuntimeException.class).given(this.pushGateway).pushAdd(this.registry, "job", this.groupingKey);
willThrow(RuntimeException.class).given(this.pushGateway).pushAdd();
this.task.getValue().run();
}

5
spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc

@ -551,16 +551,13 @@ Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exe @@ -551,16 +551,13 @@ Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exe
For ephemeral or batch jobs that may not exist long enough to be scraped, you can use https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support to expose the metrics to Prometheus.
NOTE: The Prometheus Pushgateway only works with the deprecated Prometheus simpleclient for now, until the Prometheus 1.x client adds support for it.
To switch to the simpleclient, remove `io.micrometer:micrometer-registry-prometheus` from your project and add `io.micrometer:micrometer-registry-prometheus-simpleclient` instead.
To enable Prometheus Pushgateway support, add the following dependency to your project:
[source,xml]
----
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_pushgateway</artifactId>
<artifactId>io.prometheus:prometheus-metrics-exporter-pushgateway</artifactId>
</dependency>
----

Loading…
Cancel
Save