diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a4fafba13d2..a07c2d7d41c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -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 { 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") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java index 2e79d61f8f0..5717c2db1ab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java +++ b/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 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 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 { } + /** + * Configuration for Prometheus + * Pushgateway. + */ + @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; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java index 1887d2237c5..76448adc8a0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java +++ b/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 { 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 { */ 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 { 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 { 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 + + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 7832165303a..1e95b92bbbe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java index d744c03fc69..5006501cc69 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java @@ -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; 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 @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() diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java index 1c26b2ec713..a23671b960b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java @@ -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 @@ 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 { .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 hasHttpConnectionFactory( + ThrowingConsumer 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 { diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index dd23bd47775..90a7233376d 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -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") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java index da9061c3223..a4c15250a05 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java @@ -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 { private final PushGateway pushGateway; - private final CollectorRegistry registry; - - private final String job; - - private final Map groupingKey; - private final ShutdownOperation shutdownOperation; private final TaskScheduler scheduler; @@ -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 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 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 { 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 { 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 { 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); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java index d14202c1dd9..cc14ef1c167 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java @@ -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 { @Mock private PushGateway pushGateway; - @Mock - private CollectorRegistry registry; - @Mock private TaskScheduler scheduler; private final Duration pushRate = Duration.ofSeconds(1); - private final Map groupingKey = Collections.singletonMap("foo", "bar"); - @Captor private ArgumentCaptor task; @@ -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 { @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 { @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(); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 155d3d21735..1314b3f6023 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/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 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] ---- io.prometheus - simpleclient_pushgateway + io.prometheus:prometheus-metrics-exporter-pushgateway ----