From 448b6eaad0e7af1bdab18990f91f1b04c93e6562 Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Mon, 18 Mar 2024 23:18:43 +0900 Subject: [PATCH 1/6] Auto-config support for latest Prometheus client and simpleclient Deprecates the support for simpleclient but ensures that it can work in conjunction with support for the latest Prometheus client auto-configuration. This involves breaking changes to update public classes to support the latest Prometheus client. Deprecated support for Prometheus simpleclient is provided in renamed classes. --- .../build.gradle | 1 + ...metheusMetricsExportAutoConfiguration.java | 98 +---- .../prometheus/PrometheusProperties.java | 5 +- .../PrometheusPropertiesConfigAdapter.java | 18 +- ...eclientMetricsExportAutoConfiguration.java | 163 +++++++ ...usSimpleclientPropertiesConfigAdapter.java | 64 +++ .../PrometheusExemplarsAutoConfiguration.java | 5 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...theusScrapeEndpointDocumentationTests.java | 12 +- ...lientScrapeEndpointDocumentationTests.java | 89 ++++ .../metrics/MeterRegistryCustomizerTests.java | 6 +- ...usMetricsExportAutoConfigurationTests.java | 409 ++++++++++++++++++ ...usMetricsExportAutoConfigurationTests.java | 81 ++-- ...rometheusPropertiesConfigAdapterTests.java | 9 - .../prometheus/PrometheusPropertiesTests.java | 12 +- ...ntMetricsExportAutoConfigurationTests.java | 330 ++++++++++++++ ...pleclientPropertiesConfigAdapterTests.java | 63 +++ ...etheusExemplarsAutoConfigurationTests.java | 11 +- .../spring-boot-actuator/build.gradle | 2 + .../prometheus/PrometheusOutputFormat.java | 96 ++++ .../prometheus/PrometheusScrapeEndpoint.java | 30 +- .../PrometheusSimpleclientScrapeEndpoint.java | 78 ++++ .../export/prometheus/TextOutputFormat.java | 4 +- ...metheusScrapeEndpointIntegrationTests.java | 37 +- ...eclientScrapeEndpointIntegrationTests.java | 150 +++++++ ...metheusScrapeEndpointIntegrationTests.java | 236 ++++++++++ .../spring-boot-dependencies/build.gradle | 13 +- 27 files changed, 1828 insertions(+), 195 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpointIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a3cf3a70fda3..1dc91a18178f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -56,6 +56,7 @@ dependencies { optional("io.micrometer:micrometer-registry-kairos") optional("io.micrometer:micrometer-registry-new-relic") optional("io.micrometer:micrometer-registry-otlp") + optional("io.micrometer:micrometer-registry-prometheus") optional("io.micrometer:micrometer-registry-prometheus-simpleclient") optional("io.micrometer:micrometer-registry-stackdriver") { exclude group: "commons-logging", module: "commons-logging" 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 2be7ec73bf49..7fc2832fdeab 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 @@ -16,39 +16,26 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Duration; -import java.util.Map; - import io.micrometer.core.instrument.Clock; -import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.exemplars.DefaultExemplarSampler; -import io.prometheus.client.exemplars.ExemplarSampler; -import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; -import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; -import io.prometheus.client.exporter.PushGateway; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.metrics.model.registry.PrometheusRegistry; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; 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.PrometheusPushGatewayManager.ShutdownOperation; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; 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.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. @@ -58,98 +45,43 @@ * @author Jonatan Ivanov * @since 2.0.0 */ -@SuppressWarnings("deprecation") @AutoConfiguration( before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }, after = MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) -@ConditionalOnClass(io.micrometer.prometheus.PrometheusMeterRegistry.class) +@ConditionalOnClass(PrometheusMeterRegistry.class) @ConditionalOnEnabledMetricsExport("prometheus") @EnableConfigurationProperties(PrometheusProperties.class) public class PrometheusMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - public io.micrometer.prometheus.PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperties) { + public PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperties) { return new PrometheusPropertiesConfigAdapter(prometheusProperties); } @Bean @ConditionalOnMissingBean - public io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry( - io.micrometer.prometheus.PrometheusConfig prometheusConfig, CollectorRegistry collectorRegistry, - Clock clock, ObjectProvider exemplarSamplerProvider) { - return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock, - exemplarSamplerProvider.getIfAvailable()); + public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig, + PrometheusRegistry prometheusRegistry, Clock clock) { + return new PrometheusMeterRegistry(prometheusConfig, prometheusRegistry, clock, null); } @Bean @ConditionalOnMissingBean - public CollectorRegistry collectorRegistry() { - return new CollectorRegistry(true); - } - - @Bean - @ConditionalOnMissingBean(ExemplarSampler.class) - @ConditionalOnBean(SpanContextSupplier.class) - public DefaultExemplarSampler exemplarSampler(SpanContextSupplier spanContextSupplier) { - return new DefaultExemplarSampler(spanContextSupplier); + public PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); } @Configuration(proxyBeanMethods = false) @ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint.class) public static class PrometheusScrapeEndpointConfiguration { + @SuppressWarnings("removal") @Bean - @ConditionalOnMissingBean - public PrometheusScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); - } - - } - - /** - * Configuration for Prometheus - * Pushgateway. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(PushGateway.class) - @ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled") - public 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 - public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, - PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { - PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); - Duration pushRate = properties.getPushRate(); - String job = getJob(properties, environment); - Map groupingKey = properties.getGroupingKey(); - ShutdownOperation shutdownOperation = properties.getShutdownOperation(); - PushGateway pushGateway = initializePushGateway(properties.getBaseUrl()); - if (StringUtils.hasText(properties.getUsername())) { - pushGateway.setConnectionFactory( - new BasicAuthHttpConnectionFactory(properties.getUsername(), properties.getPassword())); - } - return new PrometheusPushGatewayManager(pushGateway, collectorRegistry, pushRate, job, groupingKey, - shutdownOperation); - } - - private PushGateway initializePushGateway(String url) throws MalformedURLException { - return new PushGateway(new URL(url)); - } - - 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; + @ConditionalOnMissingBean({ PrometheusScrapeEndpoint.class, PrometheusSimpleclientScrapeEndpoint.class }) + public PrometheusScrapeEndpoint prometheusEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } } 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 c22be89e7d3c..b53f91336a6e 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 @@ -31,7 +31,6 @@ * @author Stephane Nicoll * @since 2.0.0 */ -@SuppressWarnings("deprecation") @ConfigurationProperties(prefix = "management.prometheus.metrics.export") public class PrometheusProperties { @@ -55,6 +54,8 @@ public class PrometheusProperties { /** * Histogram type for backing DistributionSummary and Timer. */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated(since = "3.3.0", forRemoval = true) private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus; /** @@ -70,10 +71,12 @@ public void setDescriptions(boolean descriptions) { this.descriptions = descriptions; } + @SuppressWarnings("deprecation") public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { return this.histogramFlavor; } + @SuppressWarnings("deprecation") public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { this.histogramFlavor = histogramFlavor; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java index 47b50a7bca2b..8ec42c009e6f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java @@ -18,18 +18,18 @@ import java.time.Duration; +import io.micrometer.prometheusmetrics.PrometheusConfig; + import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; /** - * Adapter to convert {@link PrometheusProperties} to a - * {@link io.micrometer.prometheus.PrometheusConfig}. + * Adapter to convert {@link PrometheusProperties} to a {@link PrometheusConfig}. * * @author Jon Schneider * @author Phillip Webb */ -@SuppressWarnings("deprecation") class PrometheusPropertiesConfigAdapter extends PropertiesConfigAdapter - implements io.micrometer.prometheus.PrometheusConfig { + implements PrometheusConfig { PrometheusPropertiesConfigAdapter(PrometheusProperties properties) { super(properties); @@ -47,18 +47,12 @@ public String get(String key) { @Override public boolean descriptions() { - return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions); - } - - @Override - public io.micrometer.prometheus.HistogramFlavor histogramFlavor() { - return get(PrometheusProperties::getHistogramFlavor, - io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); + return get(PrometheusProperties::isDescriptions, PrometheusConfig.super::descriptions); } @Override public Duration step() { - return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + return get(PrometheusProperties::getStep, PrometheusConfig.super::step); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java new file mode 100644 index 000000000000..544706590a24 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.Map; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exemplars.DefaultExemplarSampler; +import io.prometheus.client.exemplars.ExemplarSampler; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +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.PrometheusPushGatewayManager.ShutdownOperation; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +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.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 + * with the Prometheus simpleclient. + * + * @author Jon Schneider + * @author David J. M. Karlsen + * @author Jonatan Ivanov + * @since 2.0.0 + * @deprecated in favor of {@link PrometheusMetricsExportAutoConfiguration} + */ +@Deprecated(since = "3.3.0", forRemoval = true) +@AutoConfiguration( + before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }, + after = { MetricsAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class }) +@ConditionalOnBean(Clock.class) +@ConditionalOnClass(PrometheusMeterRegistry.class) +@ConditionalOnEnabledMetricsExport("prometheus") +@EnableConfigurationProperties(PrometheusProperties.class) +public class PrometheusSimpleclientMetricsExportAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) { + return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties); + } + + @Bean + @ConditionalOnMissingBean + public io.micrometer.prometheus.PrometheusMeterRegistry simpleclientPrometheusMeterRegistry( + io.micrometer.prometheus.PrometheusConfig prometheusConfig, CollectorRegistry collectorRegistry, + Clock clock, ObjectProvider exemplarSamplerProvider) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock, + exemplarSamplerProvider.getIfAvailable()); + } + + @Bean + @ConditionalOnMissingBean + public CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + @ConditionalOnMissingBean(ExemplarSampler.class) + @ConditionalOnBean(SpanContextSupplier.class) + public DefaultExemplarSampler exemplarSampler(SpanContextSupplier spanContextSupplier) { + return new DefaultExemplarSampler(spanContextSupplier); + } + + @SuppressWarnings("removal") + @Configuration(proxyBeanMethods = false) + @ConditionalOnAvailableEndpoint(endpoint = PrometheusSimpleclientScrapeEndpoint.class) + public static class PrometheusScrapeEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean({ PrometheusSimpleclientScrapeEndpoint.class, PrometheusScrapeEndpoint.class }) + public PrometheusSimpleclientScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + /** + * Configuration for Prometheus + * Pushgateway. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(PushGateway.class) + @ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled") + public 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 + public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, + PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { + PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); + Duration pushRate = properties.getPushRate(); + String job = getJob(properties, environment); + Map groupingKey = properties.getGroupingKey(); + ShutdownOperation shutdownOperation = properties.getShutdownOperation(); + PushGateway pushGateway = initializePushGateway(properties.getBaseUrl()); + if (StringUtils.hasText(properties.getUsername())) { + pushGateway.setConnectionFactory( + new BasicAuthHttpConnectionFactory(properties.getUsername(), properties.getPassword())); + } + return new PrometheusPushGatewayManager(pushGateway, collectorRegistry, pushRate, job, groupingKey, + shutdownOperation); + } + + private PushGateway initializePushGateway(String url) throws MalformedURLException { + return new PushGateway(new URL(url)); + } + + 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/PrometheusSimpleclientPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java new file mode 100644 index 000000000000..f39eca77303b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import java.time.Duration; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; + +/** + * Adapter to convert {@link PrometheusProperties} to a + * {@link io.micrometer.prometheus.PrometheusConfig}. + * + * @author Jon Schneider + * @author Phillip Webb + */ +@SuppressWarnings("deprecation") +class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter + implements io.micrometer.prometheus.PrometheusConfig { + + PrometheusSimpleclientPropertiesConfigAdapter(PrometheusProperties properties) { + super(properties); + } + + @Override + public String prefix() { + return "management.prometheus.metrics.export"; + } + + @Override + public String get(String key) { + return null; + } + + @Override + public boolean descriptions() { + return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions); + } + + @Override + public io.micrometer.prometheus.HistogramFlavor histogramFlavor() { + return get(PrometheusProperties::getHistogramFlavor, + io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); + } + + @Override + public Duration step() { + return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java index 4176d2c2462b..8954a05a2eb7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -21,7 +21,7 @@ import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -38,7 +38,8 @@ * @author Jonatan Ivanov * @since 3.0.0 */ -@AutoConfiguration(before = PrometheusMetricsExportAutoConfiguration.class, +@SuppressWarnings("removal") +@AutoConfiguration(before = PrometheusSimpleclientMetricsExportAutoConfiguration.class, after = MicrometerTracingAutoConfiguration.class) @ConditionalOnBean(Tracer.class) @ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 7801946776fc..970224b2c566 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -64,6 +64,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.kairos.KairosMetri org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic.NewRelicMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.signalfx.SignalFxMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration 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 d51980437b7e..429d2e34d4da 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 @@ -18,8 +18,9 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; -import io.prometheus.client.CollectorRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.client.exporter.common.TextFormat; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; @@ -72,13 +73,12 @@ void filteredPrometheus() throws Exception { static class TestConfiguration { @Bean - @SuppressWarnings("deprecation") PrometheusScrapeEndpoint endpoint() { - CollectorRegistry collectorRegistry = new CollectorRegistry(true); - io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( - (key) -> null, collectorRegistry, Clock.SYSTEM); + PrometheusRegistry prometheusRegistry = new PrometheusRegistry(); + PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((key) -> null, prometheusRegistry, + Clock.SYSTEM); new JvmMemoryMetrics().bindTo(meterRegistry); - return new PrometheusScrapeEndpoint(collectorRegistry); + return new PrometheusScrapeEndpoint(prometheusRegistry); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java new file mode 100644 index 000000000000..991a36e1dbb9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2024 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.autoconfigure.endpoint.web.documentation; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +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; + +/** + * Tests for generating documentation describing the + * {@link PrometheusSimpleclientScrapeEndpoint}. + * + * @author Andy Wilkinson + * @author Johnny Lim + */ +class PrometheusSimpleclientScrapeEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + @Test + void prometheus() throws Exception { + this.mockMvc.perform(get("/actuator/prometheus")) + .andExpect(status().isOk()) + .andDo(document("prometheus-simpleclient/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-simpleclient/openmetrics")); + } + + @Test + void filteredPrometheus() throws Exception { + this.mockMvc + .perform(get("/actuator/prometheus").param("includedNames", + "jvm_memory_used_bytes,jvm_memory_committed_bytes")) + .andExpect(status().isOk()) + .andDo(document("prometheus-simpleclient/names", + queryParameters(parameterWithName("includedNames") + .description("Restricts the samples to those that match the names. Optional.") + .optional()))); + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + @SuppressWarnings({ "removal", "deprecation" }) + PrometheusSimpleclientScrapeEndpoint endpoint() { + CollectorRegistry collectorRegistry = new CollectorRegistry(true); + io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( + (key) -> null, collectorRegistry, Clock.SYSTEM); + new JvmMemoryMetrics().bindTo(meterRegistry); + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java index 5b209ffda463..7f0783acd373 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java @@ -18,6 +18,7 @@ import io.micrometer.atlas.AtlasMeterRegistry; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.export.atlas.AtlasMetricsExportAutoConfiguration; @@ -36,7 +37,6 @@ * @author Jon Schneider * @author Andy Wilkinson */ -@SuppressWarnings("deprecation") class MeterRegistryCustomizerTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -63,7 +63,7 @@ void commonTagsAreAppliedBeforeRegistryIsInjectableElsewhere() { @Test void customizersCanBeAppliedToSpecificRegistryTypes() { this.contextRunner.withUserConfiguration(MeterRegistryCustomizerConfiguration.class).run((context) -> { - MeterRegistry prometheus = context.getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class); + MeterRegistry prometheus = context.getBean(PrometheusMeterRegistry.class); prometheus.get("jvm.memory.used").tags("job", "myjob").gauge(); MeterRegistry atlas = context.getBean(AtlasMeterRegistry.class); assertThat(atlas.find("jvm.memory.used").tags("job", "myjob").gauge()).isNull(); @@ -79,7 +79,7 @@ MeterRegistryCustomizer commonTags() { } @Bean - MeterRegistryCustomizer prometheusOnlyCommonTags() { + MeterRegistryCustomizer prometheusOnlyCommonTags() { return (registry) -> registry.config().commonTags("job", "myjob"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..9b607d834e43 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java @@ -0,0 +1,409 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exemplars.ExemplarSampler; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.DefaultHttpConnectionFactory; +import io.prometheus.client.exporter.HttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +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.metrics.export.prometheus.DualPrometheusMetricsExportAutoConfigurationTests.CustomSecondEndpointConfiguration.SecondPrometheusScrapeEndpoint; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +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; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientMetricsExportAutoConfiguration} and + * {@link PrometheusMetricsExportAutoConfiguration} with both Prometheus clients on the + * classpath. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Jonatan Ivanov + */ +@SuppressWarnings({ "removal", "deprecation" }) +@ExtendWith(OutputCaptureExtension.class) +class DualPrometheusMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, + PrometheusMetricsExportAutoConfiguration.class)); + + @Test + void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void autoConfiguresItsConfigPrometheusRegistryAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.defaults.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.prometheus.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customConfig") + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) + .hasBean("otherCustomConfig")); + } + + @Test + void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) + .hasBean("otherCustomRegistry")); + } + + @Test + void allowsCustomCollectorRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customCollectorRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customPrometheusRegistry") + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .hasSingleBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void allowsCustomExemplarSamplerToBeUsed() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) + .run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class) + .getBean(ExemplarSampler.class) + .isSameAs(context.getBean("customExemplarSampler"))); + } + + @Test + void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) + .doesNotHaveBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void addsScrapeEndpointToManagementContext() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .run((context) -> assertThat(context).hasSingleBean(PrometheusScrapeEndpoint.class) + .doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointNotAddedToManagementContextWhenNotExposed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class) + .doesNotHaveBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .withPropertyValues("management.endpoint.prometheus.enabled=false") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class) + .doesNotHaveBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void allowsCustomScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void allowsCustomSecondScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomSecondEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customSecondEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class) + .hasSingleBean(SecondPrometheusScrapeEndpoint.class) + .hasSingleBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); + } + + @Test + 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/"); + }); + } + + @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 withCustomPushGatewayURL() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); + } + + @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(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(BasicAuthHttpConnectionFactory.class))); + } + + private void hasGatewayURL(AssertableApplicationContext context, String url) { + assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + } + + 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 { + + @Bean + Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusConfig customConfig() { + return (key) -> null; + } + + @Bean + io.micrometer.prometheusmetrics.PrometheusConfig otherCustomConfig() { + return (key) -> null; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + } + + @Bean + io.micrometer.prometheusmetrics.PrometheusMeterRegistry otherCustomRegistry( + io.micrometer.prometheusmetrics.PrometheusConfig config, PrometheusRegistry prometheusRegistry, + Clock clock) { + return new io.micrometer.prometheusmetrics.PrometheusMeterRegistry(config, prometheusRegistry, clock); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomCollectorRegistryConfiguration { + + @Bean + CollectorRegistry customCollectorRegistry() { + return new CollectorRegistry(); + } + + @Bean + PrometheusRegistry customPrometheusRegistry() { + return new PrometheusRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomEndpointConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomSecondEndpointConfiguration { + + @Bean + PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); + } + + @Bean + SecondPrometheusScrapeEndpoint customSecondEndpoint(CollectorRegistry collectorRegistry) { + return new SecondPrometheusScrapeEndpoint(collectorRegistry); + } + + @WebEndpoint(id = "prometheussc") + static class SecondPrometheusScrapeEndpoint extends PrometheusSimpleclientScrapeEndpoint { + + SecondPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + super(collectorRegistry); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class ExemplarsConfiguration { + + @Bean + SpanContextSupplier spanContextSupplier() { + return new SpanContextSupplier() { + + @Override + public String getTraceId() { + return null; + } + + @Override + public String getSpanId() { + return null; + } + + @Override + public boolean isSampled() { + return false; + } + + }; + } + + } + +} 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 e691fc189744..70861cae6968 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 @@ -17,14 +17,15 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; import io.micrometer.core.instrument.Clock; -import io.prometheus.client.CollectorRegistry; import io.prometheus.client.exemplars.ExemplarSampler; import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; import io.prometheus.client.exporter.DefaultHttpConnectionFactory; import io.prometheus.client.exporter.HttpConnectionFactory; import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,6 +33,7 @@ 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.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; @@ -52,25 +54,26 @@ * @author Stephane Nicoll * @author Jonatan Ivanov */ -@SuppressWarnings("deprecation") @ExtendWith(OutputCaptureExtension.class) class PrometheusMetricsExportAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withClassLoader(new FilteredClassLoader("io.micrometer.prometheus.", "io.prometheus.client")) .withConfiguration(AutoConfigurations.of(PrometheusMetricsExportAutoConfiguration.class)); @Test void backsOffWithoutAClock() { this.contextRunner.run((context) -> assertThat(context) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)); } @Test void autoConfiguresItsConfigCollectorRegistryAndMeterRegistry() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test @@ -78,9 +81,9 @@ void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.defaults.metrics.export.enabled=false") .run((context) -> assertThat(context) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .doesNotHaveBean(CollectorRegistry.class) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test @@ -88,47 +91,52 @@ void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.prometheus.metrics.export.enabled=false") .run((context) -> assertThat(context) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .doesNotHaveBean(CollectorRegistry.class) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test void allowsCustomConfigToBeUsed() { this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) .hasBean("customConfig")); } @Test void allowsCustomRegistryToBeUsed() { this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) .hasBean("customRegistry") - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test void allowsCustomCollectorRegistryToBeUsed() { - this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .hasBean("customCollectorRegistry") - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + this.contextRunner.withUserConfiguration(CustomPrometheusRegistryConfiguration.class) + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasBean("customPrometheusRegistry") + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test + @Disabled("exemplar support with new client is not integrated in Micrometer yet") void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) .hasSingleBean(ExemplarSampler.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)); } @Test + @Disabled("exemplar support with new client is not integrated in Micrometer yet") void allowsCustomExemplarSamplerToBeUsed() { this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) @@ -142,7 +150,7 @@ void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) .doesNotHaveBean(ExemplarSampler.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)); } @Test @@ -184,6 +192,7 @@ void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { } @Test + @Disabled("new Prometheus client does not have support for Pushgateway yet") void withPushGatewayEnabled(CapturedOutput output) { this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") @@ -195,6 +204,7 @@ void withPushGatewayEnabled(CapturedOutput output) { } @Test + @Disabled("new Prometheus client does not have support for Pushgateway yet") void withPushGatewayNoBasicAuth() { this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") @@ -204,6 +214,7 @@ void withPushGatewayNoBasicAuth() { } @Test + @Disabled("new Prometheus client does not have support for Pushgateway yet") void withCustomPushGatewayURL() { this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", @@ -213,6 +224,7 @@ void withCustomPushGatewayURL() { } @Test + @Disabled("new Prometheus client does not have support for Pushgateway yet") void withPushGatewayBasicAuth() { this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", @@ -257,7 +269,7 @@ Clock clock() { static class CustomConfigConfiguration { @Bean - io.micrometer.prometheus.PrometheusConfig customConfig() { + io.micrometer.prometheusmetrics.PrometheusConfig customConfig() { return (key) -> null; } @@ -268,20 +280,21 @@ io.micrometer.prometheus.PrometheusConfig customConfig() { static class CustomRegistryConfiguration { @Bean - io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( - io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { - return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + io.micrometer.prometheusmetrics.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheusmetrics.PrometheusConfig config, PrometheusRegistry prometheusRegistry, + Clock clock) { + return new io.micrometer.prometheusmetrics.PrometheusMeterRegistry(config, prometheusRegistry, clock); } } @Configuration(proxyBeanMethods = false) @Import(BaseConfiguration.class) - static class CustomCollectorRegistryConfiguration { + static class CustomPrometheusRegistryConfiguration { @Bean - CollectorRegistry customCollectorRegistry() { - return new CollectorRegistry(); + PrometheusRegistry customPrometheusRegistry() { + return new PrometheusRegistry(); } } @@ -291,8 +304,8 @@ CollectorRegistry customCollectorRegistry() { static class CustomEndpointConfiguration { @Bean - PrometheusScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); + PrometheusScrapeEndpoint customEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java index f38efdcbc0d7..09f34b55a826 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java @@ -29,7 +29,6 @@ * * @author Mirko Sobeck */ -@SuppressWarnings("deprecation") class PrometheusPropertiesConfigAdapterTests extends AbstractPropertiesConfigAdapterTests { @@ -44,14 +43,6 @@ void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { assertThat(new PrometheusPropertiesConfigAdapter(properties).descriptions()).isFalse(); } - @Test - void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { - PrometheusProperties properties = new PrometheusProperties(); - properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); - assertThat(new PrometheusPropertiesConfigAdapter(properties).histogramFlavor()) - .isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); - } - @Test void whenPropertiesStepIsSetAdapterStepReturnsIt() { PrometheusProperties properties = new PrometheusProperties(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java index b2297f97358c..538c8c28a98a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java @@ -25,11 +25,11 @@ * * @author Stephane Nicoll */ -@SuppressWarnings("deprecation") class PrometheusPropertiesTests { + @SuppressWarnings("deprecation") @Test - void defaultValuesAreConsistent() { + void defaultValuesAreConsistentWithSimpleclient() { PrometheusProperties properties = new PrometheusProperties(); io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT; assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); @@ -37,4 +37,12 @@ void defaultValuesAreConsistent() { assertThat(properties.getStep()).isEqualTo(config.step()); } + @Test + void defaultValuesAreConsistent() { + PrometheusProperties properties = new PrometheusProperties(); + io.micrometer.prometheusmetrics.PrometheusConfig config = io.micrometer.prometheusmetrics.PrometheusConfig.DEFAULT; + assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); + assertThat(properties.getStep()).isEqualTo(config.step()); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..d409db5f7ba9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfigurationTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exemplars.ExemplarSampler; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.DefaultHttpConnectionFactory; +import io.prometheus.client.exporter.HttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; +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.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +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; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientMetricsExportAutoConfiguration}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Jonatan Ivanov + */ +@SuppressWarnings({ "removal", "deprecation" }) +@ExtendWith(OutputCaptureExtension.class) +class PrometheusSimpleclientMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withClassLoader(new FilteredClassLoader("io.micrometer.prometheusmetrics.", "io.prometheus.metrics")) + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class)); + + @Test + void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void autoConfiguresItsConfigCollectorRegistryAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.defaults.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.prometheus.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customConfig")); + } + + @Test + void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void allowsCustomCollectorRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customCollectorRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .hasSingleBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void allowsCustomExemplarSamplerToBeUsed() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) + .run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class) + .getBean(ExemplarSampler.class) + .isSameAs(context.getBean("customExemplarSampler"))); + } + + @Test + void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) + .doesNotHaveBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void addsScrapeEndpointToManagementContext() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .run((context) -> assertThat(context).hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointNotAddedToManagementContextWhenNotExposed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .withPropertyValues("management.endpoint.prometheus.enabled=false") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void allowsCustomScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); + } + + @Test + 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/"); + }); + } + + @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 withCustomPushGatewayURL() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); + } + + @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(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(BasicAuthHttpConnectionFactory.class))); + } + + private void hasGatewayURL(AssertableApplicationContext context, String url) { + assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + } + + 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 { + + @Bean + Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusConfig customConfig() { + return (key) -> null; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomCollectorRegistryConfiguration { + + @Bean + CollectorRegistry customCollectorRegistry() { + return new CollectorRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomEndpointConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class ExemplarsConfiguration { + + @Bean + SpanContextSupplier spanContextSupplier() { + return new SpanContextSupplier() { + + @Override + public String getTraceId() { + return null; + } + + @Override + public String getSpanId() { + return null; + } + + @Override + public boolean isSampled() { + return false; + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java new file mode 100644 index 000000000000..40faaab7ce5e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.AbstractPropertiesConfigAdapterTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrometheusSimpleclientPropertiesConfigAdapter}. + * + * @author Mirko Sobeck + */ +@SuppressWarnings("deprecation") +class PrometheusSimpleclientPropertiesConfigAdapterTests extends + AbstractPropertiesConfigAdapterTests { + + PrometheusSimpleclientPropertiesConfigAdapterTests() { + super(PrometheusSimpleclientPropertiesConfigAdapter.class); + } + + @Test + void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { + PrometheusProperties properties = new PrometheusProperties(); + properties.setDescriptions(false); + assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).descriptions()).isFalse(); + } + + @Test + void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { + PrometheusProperties properties = new PrometheusProperties(); + properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); + assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).histogramFlavor()) + .isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); + } + + @Test + void whenPropertiesStepIsSetAdapterStepReturnsIt() { + PrometheusProperties properties = new PrometheusProperties(); + properties.setStep(Duration.ofSeconds(30)); + assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).step()) + .isEqualTo(Duration.ofSeconds(30)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index bc6030fdf2ac..8b7b2ee3ff09 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -26,7 +26,7 @@ import io.prometheus.client.exporter.common.TextFormat; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; @@ -46,6 +46,7 @@ * * @author Jonatan Ivanov */ +@SuppressWarnings("removal") class PrometheusExemplarsAutoConfigurationTests { private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile( @@ -57,10 +58,10 @@ class PrometheusExemplarsAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("management.tracing.sampling.probability=1.0", "management.metrics.distribution.percentiles-histogram.all=true") - .with(MetricsRun.limitedTo(PrometheusMetricsExportAutoConfiguration.class)) - .withConfiguration( - AutoConfigurations.of(PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, - BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + .with(MetricsRun.limitedTo()) + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, + PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); @Test void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index c66e297035b6..0c348ca85f40 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -24,7 +24,9 @@ dependencies { optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-jakarta9") optional("io.micrometer:micrometer-tracing") + 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") } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java new file mode 100644 index 000000000000..25d18012e787 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2024 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 java.io.IOException; +import java.io.OutputStream; + +import io.prometheus.metrics.expositionformats.ExpositionFormats; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; + +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * A {@link Producible} enum for supported Prometheus formats. + * + * @author Andy Wilkinson + * @since 3.3.0 + */ +public enum PrometheusOutputFormat implements Producible { + + /** + * Prometheus text version 0.0.4. + */ + CONTENT_TYPE_004(PrometheusTextFormatWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getPrometheusTextFormatWriter().write(outputStream, snapshots); + } + + @Override + public boolean isDefault() { + return true; + } + + }, + + /** + * OpenMetrics text version 1.0.0. + */ + CONTENT_TYPE_OPENMETRICS_100(OpenMetricsTextFormatWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getOpenMetricsTextFormatWriter().write(outputStream, snapshots); + } + + }, + + /** + * Prometheus metrics protobuf. + */ + CONTENT_TYPE_PROTOBUF(PrometheusProtobufWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getPrometheusProtobufWriter().write(outputStream, snapshots); + } + + }; + + private static final ExpositionFormats EXPOSITION_FORMATS = ExpositionFormats.init(); + + private final MimeType mimeType; + + PrometheusOutputFormat(String mimeType) { + this.mimeType = MimeTypeUtils.parseMimeType(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + + abstract void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java index 9f0a1db9bd72..6991f4e1b785 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java @@ -16,14 +16,12 @@ package org.springframework.boot.actuate.metrics.export.prometheus; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Enumeration; import java.util.Set; -import io.prometheus.client.Collector.MetricFamilySamples; -import io.prometheus.client.CollectorRegistry; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -44,30 +42,28 @@ public class PrometheusScrapeEndpoint { private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024; - private final CollectorRegistry collectorRegistry; + private final PrometheusRegistry prometheusRegistry; private volatile int nextMetricsScrapeSize = 16; - public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { - this.collectorRegistry = collectorRegistry; + public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + this.prometheusRegistry = prometheusRegistry; } - @ReadOperation(producesFrom = TextOutputFormat.class) - public WebEndpointResponse scrape(TextOutputFormat format, @Nullable Set includedNames) { + @ReadOperation(producesFrom = PrometheusOutputFormat.class) + public WebEndpointResponse scrape(PrometheusOutputFormat format, @Nullable Set includedNames) { try { - Writer writer = new StringWriter(this.nextMetricsScrapeSize); - Enumeration samples = (includedNames != null) - ? this.collectorRegistry.filteredMetricFamilySamples(includedNames) - : this.collectorRegistry.metricFamilySamples(); - format.write(writer, samples); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize); + MetricSnapshots metricSnapshots = (includedNames != null) + ? this.prometheusRegistry.scrape(includedNames::contains) : this.prometheusRegistry.scrape(); + format.write(outputStream, metricSnapshots); - String scrapePage = writer.toString(); + String scrapePage = outputStream.toString(); this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; return new WebEndpointResponse<>(scrapePage, format); } catch (IOException ex) { - // This actually never happens since StringWriter doesn't throw an IOException throw new IllegalStateException("Writing metrics failed", ex); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java new file mode 100644 index 000000000000..2da3fe0d1e61 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2024 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 java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Enumeration; +import java.util.Set; + +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.CollectorRegistry; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +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.lang.Nullable; + +/** + * {@link Endpoint @Endpoint} that uses the Prometheus simpleclient to output metrics in a + * format that can be scraped by the Prometheus server. + * + * @author Jon Schneider + * @author Johnny Lim + * @since 2.0.0 + * @deprecated in favor of {@link PrometheusScrapeEndpoint} + */ +@Deprecated(since = "3.3.0", forRemoval = true) +@WebEndpoint(id = "prometheus") +public class PrometheusSimpleclientScrapeEndpoint { + + private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024; + + private final CollectorRegistry collectorRegistry; + + private volatile int nextMetricsScrapeSize = 16; + + public PrometheusSimpleclientScrapeEndpoint(CollectorRegistry collectorRegistry) { + this.collectorRegistry = collectorRegistry; + } + + @SuppressWarnings("removal") + @ReadOperation(producesFrom = TextOutputFormat.class) + public WebEndpointResponse scrape(TextOutputFormat format, @Nullable Set includedNames) { + try { + Writer writer = new StringWriter(this.nextMetricsScrapeSize); + Enumeration samples = (includedNames != null) + ? this.collectorRegistry.filteredMetricFamilySamples(includedNames) + : this.collectorRegistry.metricFamilySamples(); + format.write(writer, samples); + + String scrapePage = writer.toString(); + this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; + + return new WebEndpointResponse<>(scrapePage, format); + } + catch (IOException ex) { + // This actually never happens since StringWriter doesn't throw an IOException + throw new IllegalStateException("Writing metrics failed", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java index 54b16b7110c9..a49777ed29dd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 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. @@ -32,7 +32,9 @@ * * @author Andy Wilkinson * @since 2.5.0 + * @deprecated in favor of {@link PrometheusOutputFormat} */ +@Deprecated(since = "3.3.0", forRemoval = true) public enum TextOutputFormat implements Producible { /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java index 49c3a658db36..de7aaa643b57 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java @@ -19,8 +19,10 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; -import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.exporter.common.TextFormat; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.context.annotation.Bean; @@ -40,8 +42,7 @@ class PrometheusScrapeEndpointIntegrationTests { @WebEndpointTest void scrapeHasContentTypeText004ByDefault(WebTestClient client) { - String expectedContentType = TextFormat.CONTENT_TYPE_004; - assertThat(TextFormat.chooseContentType(null)).isEqualTo(expectedContentType); + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; client.get() .uri("/actuator/prometheus") .exchange() @@ -57,9 +58,8 @@ void scrapeHasContentTypeText004ByDefault(WebTestClient client) { @WebEndpointTest void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { - String expectedContentType = TextFormat.CONTENT_TYPE_004; + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; String accept = "*/*;q=0.8"; - assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType); client.get() .uri("/actuator/prometheus") .accept(MediaType.parseMediaType(accept)) @@ -76,7 +76,7 @@ void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter( @WebEndpointTest void scrapeCanProduceOpenMetrics100(WebTestClient client) { - MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); client.get() .uri("/actuator/prometheus") .accept(openMetrics) @@ -93,8 +93,8 @@ void scrapeCanProduceOpenMetrics100(WebTestClient client) { @WebEndpointTest void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { - MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); - MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004); + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + MediaType textPlain = MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE); client.get() .uri("/actuator/prometheus") .accept(openMetrics, textPlain) @@ -108,12 +108,12 @@ void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { @WebEndpointTest void scrapeWithIncludedNames(WebTestClient client) { client.get() - .uri("/actuator/prometheus?includedNames=counter1_total,counter2_total") + .uri("/actuator/prometheus?includedNames=counter1,counter2") .exchange() .expectStatus() .isOk() .expectHeader() - .contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)) + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) .expectBody(String.class) .value((body) -> assertThat(body).contains("counter1_total") .contains("counter2_total") @@ -124,20 +124,19 @@ void scrapeWithIncludedNames(WebTestClient client) { static class TestConfiguration { @Bean - PrometheusScrapeEndpoint prometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); + PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } @Bean - CollectorRegistry collectorRegistry() { - return new CollectorRegistry(true); + PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); } @Bean - @SuppressWarnings("deprecation") - MeterRegistry registry(CollectorRegistry registry) { - io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( - (k) -> null, registry, Clock.SYSTEM); + MeterRegistry registry(PrometheusRegistry prometheusRegistry) { + PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((k) -> null, prometheusRegistry, + Clock.SYSTEM); Counter.builder("counter1").register(meterRegistry); Counter.builder("counter2").register(meterRegistry); Counter.builder("counter3").register(meterRegistry); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpointIntegrationTests.java new file mode 100644 index 000000000000..94766f193e8c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpointIntegrationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2024 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.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrometheusSimpleclientScrapeEndpoint}. + * + * @author Jon Schneider + * @author Johnny Lim + */ +@SuppressWarnings("removal") +class PrometheusSimpleclientScrapeEndpointIntegrationTests { + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefault(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + assertThat(TextFormat.chooseContentType(null)).isEqualTo(expectedContentType); + client.get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + String accept = "*/*;q=0.8"; + assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType); + client.get() + .uri("/actuator/prometheus") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_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 + void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + } + + @WebEndpointTest + void scrapeWithIncludedNames(WebTestClient client) { + client.get() + .uri("/actuator/prometheus?includedNames=counter1_total,counter2_total") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint prometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + @Bean + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + @SuppressWarnings("deprecation") + MeterRegistry registry(CollectorRegistry registry) { + io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( + (k) -> null, registry, Clock.SYSTEM); + Counter.builder("counter1").register(meterRegistry); + Counter.builder("counter2").register(meterRegistry); + Counter.builder("counter3").register(meterRegistry); + return meterRegistry; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java new file mode 100644 index 000000000000..b77c73f6e4df --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2012-2024 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.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; + +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for exposing a {@link PrometheusScrapeEndpoint} and + * {@link PrometheusSimpleclientScrapeEndpoint} with different IDs. + * + * @author Jon Schneider + * @author Johnny Lim + */ +class SecondCustomPrometheusScrapeEndpointIntegrationTests { + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefault(WebTestClient client) { + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; + client.get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + client.get() + .uri("/actuator/prometheussc") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; + String accept = "*/*;q=0.8"; + client.get() + .uri("/actuator/prometheus") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + client.get() + .uri("/actuator/prometheussc") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeCanProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + 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")); + client.get() + .uri("/actuator/prometheussc") + .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 + void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + MediaType textPlain = MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + client.get() + .uri("/actuator/prometheussc") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + } + + @WebEndpointTest + void scrapeWithIncludedNames(WebTestClient client) { + client.get() + .uri("/actuator/prometheus?includedNames=counter1,counter2") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + client.get() + .uri("/actuator/prometheussc?includedNames=counter1_total,counter2_total") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + } + + @SuppressWarnings({ "deprecation", "removal" }) + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); + } + + @Bean + CustomPrometheusScrapeEndpoint customPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + return new CustomPrometheusScrapeEndpoint(collectorRegistry); + } + + @Bean + PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); + } + + @Bean + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + PrometheusMeterRegistry registry(PrometheusRegistry prometheusRegistry) { + return new PrometheusMeterRegistry((k) -> null, prometheusRegistry, Clock.SYSTEM); + } + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry oldRegistry(CollectorRegistry collectorRegistry) { + return new io.micrometer.prometheus.PrometheusMeterRegistry((k) -> null, collectorRegistry, Clock.SYSTEM); + } + + @Bean + CompositeMeterRegistry compositeMeterRegistry(PrometheusMeterRegistry prometheusMeterRegistry, + io.micrometer.prometheus.PrometheusMeterRegistry prometheusSCMeterRegistry) { + CompositeMeterRegistry composite = new CompositeMeterRegistry(); + composite.add(prometheusMeterRegistry).add(prometheusSCMeterRegistry); + Counter.builder("counter1").register(composite); + Counter.builder("counter2").register(composite); + Counter.builder("counter3").register(composite); + return composite; + } + + @WebEndpoint(id = "prometheussc") + static class CustomPrometheusScrapeEndpoint extends PrometheusSimpleclientScrapeEndpoint { + + CustomPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + super(collectorRegistry); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 48142fe4914a..61c5cfb4339c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1415,7 +1415,18 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "0.16.0") { + library("Prometheus Client", "1.1.0") { + group("io.prometheus") { + modules = [ + "prometheus-metrics-exposition-formats" + ] + } + links { + site("https://github.com/prometheus/client_java") + releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") + } + } + library("Prometheus Simpleclient", "0.16.0") { group("io.prometheus") { imports = [ "simpleclient_bom" From dacb98a058c28aa2f592a3fdf2a8ea7df8db9b11 Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:28:13 +0900 Subject: [PATCH 2/6] Switch to snapshots and support exemplars Restores support for exemplars with the latest micrometer-registry-prometheus snapshots, depending on changes in the Prometheus client 1.2.0. Also duplicates the ConfigurationProperties class PrometheusProperties to avoid a NoClassDefFoundError when only micrometer-registry-prometheus is on the classpath. --- .../build.gradle | 1 + ...metheusMetricsExportAutoConfiguration.java | 6 +- .../prometheus/PrometheusProperties.java | 141 ------------ ...eclientMetricsExportAutoConfiguration.java | 12 +- .../PrometheusSimpleclientProperties.java | 213 ++++++++++++++++++ ...usSimpleclientPropertiesConfigAdapter.java | 15 +- .../PrometheusExemplarsAutoConfiguration.java | 29 +-- ...impleclientExemplarsAutoConfiguration.java | 96 ++++++++ ...usMetricsExportAutoConfigurationTests.java | 41 ++-- .../prometheus/PrometheusPropertiesTests.java | 10 - ...pleclientPropertiesConfigAdapterTests.java | 10 +- ...PrometheusSimpleclientPropertiesTests.java | 41 ++++ .../LazyTracingSpanContextSupplierTests.java | 7 +- .../LazyTracingSpanContextTests.java | 150 ++++++++++++ ...etheusExemplarsAutoConfigurationTests.java | 92 +++++--- ...clientExemplarsAutoConfigurationTests.java | 135 +++++++++++ .../spring-boot-dependencies/build.gradle | 8 +- 17 files changed, 756 insertions(+), 251 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 1dc91a18178f..8a31a14a7dc0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -145,6 +145,7 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("io.micrometer:micrometer-observation-test") testImplementation("io.projectreactor:reactor-test") + testImplementation("io.prometheus:prometheus-metrics-exposition-formats") testImplementation("io.r2dbc:r2dbc-h2") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.jayway.jsonpath:json-path") 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 7fc2832fdeab..c876428d2221 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 @@ -20,7 +20,9 @@ import io.micrometer.prometheusmetrics.PrometheusConfig; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.tracer.common.SpanContext; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; @@ -63,8 +65,8 @@ public PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperti @Bean @ConditionalOnMissingBean public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig, - PrometheusRegistry prometheusRegistry, Clock clock) { - return new PrometheusMeterRegistry(prometheusConfig, prometheusRegistry, clock, null); + PrometheusRegistry prometheusRegistry, Clock clock, ObjectProvider spanContext) { + return new PrometheusMeterRegistry(prometheusConfig, prometheusRegistry, clock, spanContext.getIfAvailable()); } @Bean 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 b53f91336a6e..8002db73303a 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 @@ -17,10 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -45,19 +42,6 @@ public class PrometheusProperties { */ private boolean descriptions = true; - /** - * Configuration options for using Prometheus Pushgateway, allowing metrics to be - * pushed when they cannot be scraped. - */ - private final Pushgateway pushgateway = new Pushgateway(); - - /** - * Histogram type for backing DistributionSummary and Timer. - */ - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated(since = "3.3.0", forRemoval = true) - private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus; - /** * Step size (i.e. reporting frequency) to use. */ @@ -71,16 +55,6 @@ public void setDescriptions(boolean descriptions) { this.descriptions = descriptions; } - @SuppressWarnings("deprecation") - public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { - return this.histogramFlavor; - } - - @SuppressWarnings("deprecation") - public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { - this.histogramFlavor = histogramFlavor; - } - public Duration getStep() { return this.step; } @@ -97,119 +71,4 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public Pushgateway getPushgateway() { - return this.pushgateway; - } - - /** - * Configuration options for push-based interaction with Prometheus. - */ - public static class Pushgateway { - - /** - * Enable publishing over a Prometheus Pushgateway. - */ - private Boolean enabled = false; - - /** - * Base URL for the Pushgateway. - */ - private String baseUrl = "http://localhost:9091"; - - /** - * Login user of the Prometheus Pushgateway. - */ - private String username; - - /** - * Login password of the Prometheus Pushgateway. - */ - private String password; - - /** - * Frequency with which to push metrics. - */ - private Duration pushRate = Duration.ofMinutes(1); - - /** - * Job identifier for this application instance. - */ - private String job; - - /** - * Grouping key for the pushed metrics. - */ - private Map groupingKey = new HashMap<>(); - - /** - * Operation that should be performed on shutdown. - */ - private ShutdownOperation shutdownOperation = ShutdownOperation.NONE; - - public Boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getPushRate() { - return this.pushRate; - } - - public void setPushRate(Duration pushRate) { - this.pushRate = pushRate; - } - - public String getJob() { - return this.job; - } - - public void setJob(String job) { - this.job = job; - } - - public Map getGroupingKey() { - return this.groupingKey; - } - - public void setGroupingKey(Map groupingKey) { - this.groupingKey = groupingKey; - } - - public ShutdownOperation getShutdownOperation() { - return this.shutdownOperation; - } - - public void setShutdownOperation(ShutdownOperation shutdownOperation) { - this.shutdownOperation = shutdownOperation; - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java index 544706590a24..cf96499b56fd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java @@ -63,6 +63,7 @@ * @since 2.0.0 * @deprecated in favor of {@link PrometheusMetricsExportAutoConfiguration} */ +@SuppressWarnings("removal") @Deprecated(since = "3.3.0", forRemoval = true) @AutoConfiguration( before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }, @@ -70,12 +71,12 @@ @ConditionalOnBean(Clock.class) @ConditionalOnClass(PrometheusMeterRegistry.class) @ConditionalOnEnabledMetricsExport("prometheus") -@EnableConfigurationProperties(PrometheusProperties.class) +@EnableConfigurationProperties(PrometheusSimpleclientProperties.class) public class PrometheusSimpleclientMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - public PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) { + public PrometheusConfig simpleclientPrometheusConfig(PrometheusSimpleclientProperties prometheusProperties) { return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties); } @@ -133,8 +134,9 @@ public static class PrometheusPushGatewayConfiguration { @Bean @ConditionalOnMissingBean public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, - PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { - PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); + PrometheusSimpleclientProperties prometheusProperties, Environment environment) + throws MalformedURLException { + PrometheusSimpleclientProperties.Pushgateway properties = prometheusProperties.getPushgateway(); Duration pushRate = properties.getPushRate(); String job = getJob(properties, environment); Map groupingKey = properties.getGroupingKey(); @@ -152,7 +154,7 @@ private PushGateway initializePushGateway(String url) throws MalformedURLExcepti return new PushGateway(new URL(url)); } - private String getJob(PrometheusProperties.Pushgateway properties, Environment environment) { + private String getJob(PrometheusSimpleclientProperties.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/PrometheusSimpleclientProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java new file mode 100644 index 000000000000..d7772410198a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for configuring metrics export + * to Prometheus. + * + * @author Jon Schneider + * @author Stephane Nicoll + * @since 2.0.0 + * @deprecated use {@link PrometheusProperties} instead + */ +@Deprecated(forRemoval = true, since = "3.3.0") +@ConfigurationProperties(prefix = "management.prometheus.metrics.export") +public class PrometheusSimpleclientProperties { + + /** + * Whether exporting of metrics to this backend is enabled. + */ + private boolean enabled = true; + + /** + * Whether to enable publishing descriptions as part of the scrape payload to + * Prometheus. Turn this off to minimize the amount of data sent on each scrape. + */ + private boolean descriptions = true; + + /** + * Configuration options for using Prometheus Pushgateway, allowing metrics to be + * pushed when they cannot be scraped. + */ + private final Pushgateway pushgateway = new Pushgateway(); + + /** + * Histogram type for backing DistributionSummary and Timer. + */ + private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus; + + /** + * Step size (i.e. reporting frequency) to use. + */ + private Duration step = Duration.ofMinutes(1); + + public boolean isDescriptions() { + return this.descriptions; + } + + public void setDescriptions(boolean descriptions) { + this.descriptions = descriptions; + } + + public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { + return this.histogramFlavor; + } + + public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { + this.histogramFlavor = histogramFlavor; + } + + public Duration getStep() { + return this.step; + } + + public void setStep(Duration step) { + this.step = step; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Pushgateway getPushgateway() { + return this.pushgateway; + } + + /** + * Configuration options for push-based interaction with Prometheus. + */ + public static class Pushgateway { + + /** + * Enable publishing over a Prometheus Pushgateway. + */ + private Boolean enabled = false; + + /** + * Base URL for the Pushgateway. + */ + private String baseUrl = "http://localhost:9091"; + + /** + * Login user of the Prometheus Pushgateway. + */ + private String username; + + /** + * Login password of the Prometheus Pushgateway. + */ + private String password; + + /** + * Frequency with which to push metrics. + */ + private Duration pushRate = Duration.ofMinutes(1); + + /** + * Job identifier for this application instance. + */ + private String job; + + /** + * Grouping key for the pushed metrics. + */ + private Map groupingKey = new HashMap<>(); + + /** + * Operation that should be performed on shutdown. + */ + private ShutdownOperation shutdownOperation = ShutdownOperation.NONE; + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Duration getPushRate() { + return this.pushRate; + } + + public void setPushRate(Duration pushRate) { + this.pushRate = pushRate; + } + + public String getJob() { + return this.job; + } + + public void setJob(String job) { + this.job = job; + } + + public Map getGroupingKey() { + return this.groupingKey; + } + + public void setGroupingKey(Map groupingKey) { + this.groupingKey = groupingKey; + } + + public ShutdownOperation getShutdownOperation() { + return this.shutdownOperation; + } + + public void setShutdownOperation(ShutdownOperation shutdownOperation) { + this.shutdownOperation = shutdownOperation; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java index f39eca77303b..eb889c5464ac 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java @@ -21,17 +21,17 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; /** - * Adapter to convert {@link PrometheusProperties} to a + * Adapter to convert {@link PrometheusSimpleclientProperties} to a * {@link io.micrometer.prometheus.PrometheusConfig}. * * @author Jon Schneider * @author Phillip Webb */ -@SuppressWarnings("deprecation") -class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter +@SuppressWarnings({ "deprecation", "removal" }) +class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter implements io.micrometer.prometheus.PrometheusConfig { - PrometheusSimpleclientPropertiesConfigAdapter(PrometheusProperties properties) { + PrometheusSimpleclientPropertiesConfigAdapter(PrometheusSimpleclientProperties properties) { super(properties); } @@ -47,18 +47,19 @@ public String get(String key) { @Override public boolean descriptions() { - return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions); + return get(PrometheusSimpleclientProperties::isDescriptions, + io.micrometer.prometheus.PrometheusConfig.super::descriptions); } @Override public io.micrometer.prometheus.HistogramFlavor histogramFlavor() { - return get(PrometheusProperties::getHistogramFlavor, + return get(PrometheusSimpleclientProperties::getHistogramFlavor, io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); } @Override public Duration step() { - return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + return get(PrometheusSimpleclientProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java index 8954a05a2eb7..54bc3fa255b4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -18,10 +18,10 @@ import io.micrometer.tracing.Span; import io.micrometer.tracing.Tracer; -import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.metrics.tracer.common.SpanContext; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -38,46 +38,45 @@ * @author Jonatan Ivanov * @since 3.0.0 */ -@SuppressWarnings("removal") -@AutoConfiguration(before = PrometheusSimpleclientMetricsExportAutoConfiguration.class, +@AutoConfiguration(before = PrometheusMetricsExportAutoConfiguration.class, after = MicrometerTracingAutoConfiguration.class) @ConditionalOnBean(Tracer.class) -@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) +@ConditionalOnClass({ Tracer.class, SpanContext.class }) public class PrometheusExemplarsAutoConfiguration { @Bean @ConditionalOnMissingBean - SpanContextSupplier spanContextSupplier(ObjectProvider tracerProvider) { - return new LazyTracingSpanContextSupplier(tracerProvider); + SpanContext spanContext(ObjectProvider tracerProvider) { + return new LazyTracingSpanContext(tracerProvider); } /** * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the * {@link Tracer} can depend on the MeterRegistry (recording metrics), this - * {@link SpanContextSupplier} breaks the cycle by lazily loading the {@link Tracer}. + * {@link SpanContext} breaks the cycle by lazily loading the {@link Tracer}. */ - static class LazyTracingSpanContextSupplier implements SpanContextSupplier { + static class LazyTracingSpanContext implements SpanContext { private final SingletonSupplier tracer; - LazyTracingSpanContextSupplier(ObjectProvider tracerProvider) { + LazyTracingSpanContext(ObjectProvider tracerProvider) { this.tracer = SingletonSupplier.of(tracerProvider::getObject); } @Override - public String getTraceId() { + public String getCurrentTraceId() { Span currentSpan = currentSpan(); return (currentSpan != null) ? currentSpan.context().traceId() : null; } @Override - public String getSpanId() { + public String getCurrentSpanId() { Span currentSpan = currentSpan(); return (currentSpan != null) ? currentSpan.context().spanId() : null; } @Override - public boolean isSampled() { + public boolean isCurrentSpanSampled() { Span currentSpan = currentSpan(); if (currentSpan == null) { return false; @@ -86,6 +85,10 @@ public boolean isSampled() { return sampled != null && sampled; } + @Override + public void markCurrentSpanAsExemplar() { + } + private Span currentSpan() { return this.tracer.obtain().currentSpan(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java new file mode 100644 index 000000000000..5ce8bbe8f953 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2023 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.autoconfigure.tracing.prometheus; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; +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.context.annotation.Bean; +import org.springframework.util.function.SingletonSupplier; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Prometheus Exemplars with + * Micrometer Tracing. + * + * @author Jonatan Ivanov + * @since 3.0.0 + */ +@SuppressWarnings("removal") +@Deprecated(forRemoval = true, since = "3.3.0") +@AutoConfiguration(before = PrometheusSimpleclientMetricsExportAutoConfiguration.class, + after = MicrometerTracingAutoConfiguration.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) +public class PrometheusSimpleclientExemplarsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + SpanContextSupplier spanContextSupplier(ObjectProvider tracerProvider) { + return new LazyTracingSpanContextSupplier(tracerProvider); + } + + /** + * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the + * {@link Tracer} can depend on the MeterRegistry (recording metrics), this + * {@link SpanContextSupplier} breaks the cycle by lazily loading the {@link Tracer}. + */ + static class LazyTracingSpanContextSupplier implements SpanContextSupplier { + + private final SingletonSupplier tracer; + + LazyTracingSpanContextSupplier(ObjectProvider tracerProvider) { + this.tracer = SingletonSupplier.of(tracerProvider::getObject); + } + + @Override + public String getTraceId() { + Span currentSpan = currentSpan(); + return (currentSpan != null) ? currentSpan.context().traceId() : null; + } + + @Override + public String getSpanId() { + Span currentSpan = currentSpan(); + return (currentSpan != null) ? currentSpan.context().spanId() : null; + } + + @Override + public boolean isSampled() { + Span currentSpan = currentSpan(); + if (currentSpan == null) { + return false; + } + Boolean sampled = currentSpan.context().sampled(); + return sampled != null && sampled; + } + + private Span currentSpan() { + return this.tracer.obtain().currentSpan(); + } + + } + +} 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 70861cae6968..3c4cd15275c9 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 @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; import io.micrometer.core.instrument.Clock; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.client.exemplars.ExemplarSampler; import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; @@ -24,6 +25,7 @@ import io.prometheus.client.exporter.HttpConnectionFactory; import io.prometheus.client.exporter.PushGateway; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.tracer.common.SpanContext; import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -127,30 +129,10 @@ void allowsCustomCollectorRegistryToBeUsed() { } @Test - @Disabled("exemplar support with new client is not integrated in Micrometer yet") - void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + void autoConfiguresPrometheusMeterRegistryIfSpanContextIsPresent() { this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) - .hasSingleBean(ExemplarSampler.class) - .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)); - } - - @Test - @Disabled("exemplar support with new client is not integrated in Micrometer yet") - void allowsCustomExemplarSamplerToBeUsed() { - this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) - .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) - .run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class) - .getBean(ExemplarSampler.class) - .isSameAs(context.getBean("customExemplarSampler"))); - } - - @Test - void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { - this.contextRunner.withUserConfiguration(BaseConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) - .doesNotHaveBean(ExemplarSampler.class) - .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)); + .run((context) -> assertThat(context).hasSingleBean(SpanContext.class) + .hasSingleBean(PrometheusMeterRegistry.class)); } @Test @@ -315,24 +297,27 @@ PrometheusScrapeEndpoint customEndpoint(PrometheusRegistry prometheusRegistry) { static class ExemplarsConfiguration { @Bean - SpanContextSupplier spanContextSupplier() { - return new SpanContextSupplier() { + SpanContext spanContext() { + return new SpanContext() { @Override - public String getTraceId() { + public String getCurrentTraceId() { return null; } @Override - public String getSpanId() { + public String getCurrentSpanId() { return null; } @Override - public boolean isSampled() { + public boolean isCurrentSpanSampled() { return false; } + @Override + public void markCurrentSpanAsExemplar() { + } }; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java index 538c8c28a98a..f41c8ede9b49 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java @@ -27,16 +27,6 @@ */ class PrometheusPropertiesTests { - @SuppressWarnings("deprecation") - @Test - void defaultValuesAreConsistentWithSimpleclient() { - PrometheusProperties properties = new PrometheusProperties(); - io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT; - assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); - assertThat(properties.getHistogramFlavor()).isEqualTo(config.histogramFlavor()); - assertThat(properties.getStep()).isEqualTo(config.step()); - } - @Test void defaultValuesAreConsistent() { PrometheusProperties properties = new PrometheusProperties(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java index 40faaab7ce5e..0b6dbea932a5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java @@ -29,9 +29,9 @@ * * @author Mirko Sobeck */ -@SuppressWarnings("deprecation") +@SuppressWarnings({ "deprecation", "removal" }) class PrometheusSimpleclientPropertiesConfigAdapterTests extends - AbstractPropertiesConfigAdapterTests { + AbstractPropertiesConfigAdapterTests { PrometheusSimpleclientPropertiesConfigAdapterTests() { super(PrometheusSimpleclientPropertiesConfigAdapter.class); @@ -39,14 +39,14 @@ class PrometheusSimpleclientPropertiesConfigAdapterTests extends @Test void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { - PrometheusProperties properties = new PrometheusProperties(); + PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); properties.setDescriptions(false); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).descriptions()).isFalse(); } @Test void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { - PrometheusProperties properties = new PrometheusProperties(); + PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).histogramFlavor()) .isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); @@ -54,7 +54,7 @@ void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { @Test void whenPropertiesStepIsSetAdapterStepReturnsIt() { - PrometheusProperties properties = new PrometheusProperties(); + PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); properties.setStep(Duration.ofSeconds(30)); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).step()) .isEqualTo(Duration.ofSeconds(30)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java new file mode 100644 index 000000000000..3eb38ebb327f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrometheusSimpleclientProperties}. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("removal") +class PrometheusSimpleclientPropertiesTests { + + @SuppressWarnings("deprecation") + @Test + void defaultValuesAreConsistentWithSimpleclient() { + PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); + io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT; + assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); + assertThat(properties.getHistogramFlavor()).isEqualTo(config.histogramFlavor()); + assertThat(properties.getStep()).isEqualTo(config.step()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java index 2cc20b2ebb3d..fde7e4d62859 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java @@ -23,17 +23,18 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContextSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link LazyTracingSpanContextSupplier}. + * Tests for + * {@link org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier}. * * @author Andy Wilkinson */ +@SuppressWarnings("removal") class LazyTracingSpanContextSupplierTests { private final Tracer tracer = mock(Tracer.class); @@ -62,7 +63,7 @@ public Tracer getIfUnique() throws BeansException { }; - private final LazyTracingSpanContextSupplier spanContextSupplier = new LazyTracingSpanContextSupplier( + private final org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier spanContextSupplier = new org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier( this.objectProvider); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java new file mode 100644 index 000000000000..ffee8531bb70 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2022 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.autoconfigure.tracing.prometheus; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyTracingSpanContext}. + * + * @author Andy Wilkinson + */ +class LazyTracingSpanContextTests { + + private final Tracer tracer = mock(Tracer.class); + + private final ObjectProvider objectProvider = new ObjectProvider<>() { + + @Override + public Tracer getObject() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getObject(Object... args) throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getIfAvailable() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getIfUnique() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + }; + + private final LazyTracingSpanContext spanContextSupplier = new LazyTracingSpanContext(this.objectProvider); + + @Test + void whenCurrentSpanIsNullThenSpanIdIsNull() { + assertThat(this.spanContextSupplier.getCurrentSpanId()).isNull(); + } + + @Test + void whenCurrentSpanIsNullThenTraceIdIsNull() { + assertThat(this.spanContextSupplier.getCurrentTraceId()).isNull(); + } + + @Test + void whenCurrentSpanIsNullThenSampledIsFalse() { + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + + @Test + void whenCurrentSpanHasSpanIdThenSpanIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.spanId()).willReturn("span-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentSpanId()).isEqualTo("span-id"); + } + + @Test + void whenCurrentSpanHasTraceIdThenTraceIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.traceId()).willReturn("trace-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentTraceId()).isEqualTo("trace-id"); + } + + @Test + void whenCurrentSpanHasNoSpanIdThenSpanIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentSpanId()).isNull(); + } + + @Test + void whenCurrentSpanHasNoTraceIdThenTraceIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentTraceId()).isNull(); + } + + @Test + void whenCurrentSpanIsSampledThenSampledIsTrue() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isTrue(); + } + + @Test + void whenCurrentSpanIsNotSampledThenSampledIsFalse() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(false); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + + @Test + void whenCurrentSpanHasDeferredSamplingThenSampledIsFalse() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(null); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index 8b7b2ee3ff09..08a3793783b6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -22,11 +22,13 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; -import io.prometheus.client.exporter.common.TextFormat; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.tracer.common.SpanContext; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; @@ -46,58 +48,55 @@ * * @author Jonatan Ivanov */ -@SuppressWarnings("removal") class PrometheusExemplarsAutoConfigurationTests { private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile( - "^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + "^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); - private static final Pattern COUNTER_TRACE_INFO_PATTERN = Pattern.compile( - "^test_observation_seconds_count\\{error=\"none\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + private static final Pattern COUNT_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_count\\{error=\"none\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("management.tracing.sampling.probability=1.0", "management.metrics.distribution.percentiles-histogram.all=true") - .with(MetricsRun.limitedTo()) - .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, - PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, - BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + .with(MetricsRun.limitedTo(PrometheusMetricsExportAutoConfiguration.class)) + .withConfiguration( + AutoConfigurations.of(PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); @Test void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { - this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars")) - .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.metrics.tracer")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); } @Test void shouldNotSupplyBeansIfMicrometerTracingIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) - .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + .run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class)); } @Test void shouldSupplyCustomBeans() { this.contextRunner.withUserConfiguration(CustomConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) - .getBean(SpanContextSupplier.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContext.class) + .getBean(SpanContext.class) .isSameAs(CustomConfiguration.SUPPLIER)); } @Test - @SuppressWarnings("deprecation") void prometheusOpenMetricsOutputShouldContainExemplars() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(SpanContextSupplier.class); + assertThat(context).hasSingleBean(SpanContext.class); ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test.observation", observationRegistry).stop(); - io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry = context - .getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class); - String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(1); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(1); Optional bucketTraceInfo = openMetricsOutput.lines() .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) @@ -106,24 +105,51 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) .findFirst(); - Optional counterTraceInfo = openMetricsOutput.lines() - .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) - .map(COUNTER_TRACE_INFO_PATTERN::matcher) - .flatMap(Matcher::results) - .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) - .findFirst(); - - assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); + assertThat(bucketTraceInfo).isNotEmpty(); }); } + @Test + void prometheusOpenMetricsOutputCanBeConfiguredToContainExemplarsOnHistogramCount() { + this.contextRunner.withSystemProperties("io.prometheus.exporter.exemplarsOnAllMetricTypes=true") + .run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); + System.out.println(openMetricsOutput); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + Optional counterTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) + .map(COUNT_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); + }); + } + @Configuration(proxyBeanMethods = false) private static final class CustomConfiguration { - static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class); + static final SpanContext SUPPLIER = mock(SpanContext.class); @Bean - SpanContextSupplier customSpanContextSupplier() { + SpanContext customSpanContext() { return SUPPLIER; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000000..b16f687532fa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2024 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.autoconfigure.tracing.prometheus; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.common.TextFormat; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientExemplarsAutoConfiguration}. + * + * @author Jonatan Ivanov + */ +@SuppressWarnings("removal") +class PrometheusSimpleclientExemplarsAutoConfigurationTests { + + private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + + private static final Pattern COUNTER_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_count\\{error=\"none\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.sampling.probability=1.0", + "management.metrics.distribution.percentiles-histogram.all=true") + .with(MetricsRun.limitedTo()) + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, + PrometheusSimpleclientExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldNotSupplyBeansIfMicrometerTracingIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldSupplyCustomBeans() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .getBean(SpanContextSupplier.class) + .isSameAs(CustomConfiguration.SUPPLIER)); + } + + @Test + @SuppressWarnings("deprecation") + void prometheusOpenMetricsOutputShouldContainExemplars() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(SpanContextSupplier.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry = context + .getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + Optional counterTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) + .map(COUNTER_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); + }); + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomConfiguration { + + static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class); + + @Bean + SpanContextSupplier customSpanContextSupplier() { + return SUPPLIER; + } + + } + + private record TraceInfo(String traceId, String spanId) { + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 61c5cfb4339c..baec22c1d7a4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1232,7 +1232,7 @@ bom { ] } } - library("Micrometer", "1.13.0-M2") { + library("Micrometer", "1.13.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ @@ -1415,10 +1415,10 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.1.0") { + library("Prometheus Client", "1.2.0") { group("io.prometheus") { - modules = [ - "prometheus-metrics-exposition-formats" + imports = [ + "prometheus-metrics-bom" ] } links { From 48a3482787ec8bb6cdb28772ab09b3bf33d650b8 Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Mon, 1 Apr 2024 20:13:27 +0900 Subject: [PATCH 3/6] Add missing new autoconfig import entry --- ....springframework.boot.autoconfigure.AutoConfiguration.imports | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 970224b2c566..722368cd6277 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -109,6 +109,7 @@ org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfigurati org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontAutoConfiguration From eae66a6d30d54e8cad5705321421a2756754ae3c Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:14:01 +0900 Subject: [PATCH 4/6] Fix checkstyle and remove Pushgateway tests --- ...usMetricsExportAutoConfigurationTests.java | 79 ------------------- ...etheusExemplarsAutoConfigurationTests.java | 1 - 2 files changed, 80 deletions(-) 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 3c4cd15275c9..319ee2393f05 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 @@ -18,36 +18,21 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; -import io.prometheus.client.exemplars.ExemplarSampler; -import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; -import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; -import io.prometheus.client.exporter.DefaultHttpConnectionFactory; -import io.prometheus.client.exporter.HttpConnectionFactory; -import io.prometheus.client.exporter.PushGateway; import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.tracer.common.SpanContext; -import org.assertj.core.api.ThrowingConsumer; -import org.junit.jupiter.api.Disabled; 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.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; -import static org.mockito.Mockito.mock; /** * Tests for {@link PrometheusMetricsExportAutoConfiguration}. @@ -56,7 +41,6 @@ * @author Stephane Nicoll * @author Jonatan Ivanov */ -@ExtendWith(OutputCaptureExtension.class) class PrometheusMetricsExportAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -173,69 +157,6 @@ void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); } - @Test - @Disabled("new Prometheus client does not have support for Pushgateway yet") - 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/"); - }); - } - - @Test - @Disabled("new Prometheus client does not have support for Pushgateway yet") - 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 - @Disabled("new Prometheus client does not have support for Pushgateway yet") - void withCustomPushGatewayURL() { - this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) - .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", - "management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080") - .withUserConfiguration(BaseConfiguration.class) - .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); - } - - @Test - @Disabled("new Prometheus client does not have support for Pushgateway yet") - 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(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) - .isInstanceOf(BasicAuthHttpConnectionFactory.class))); - } - - private void hasGatewayURL(AssertableApplicationContext context, String url) { - assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); - } - - 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-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index 08a3793783b6..6aae14d135cf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -24,7 +24,6 @@ import io.micrometer.observation.ObservationRegistry; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; -import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; import io.prometheus.metrics.tracer.common.SpanContext; import org.junit.jupiter.api.Test; From 70089c15e611158e92007a434a3a38a12ec1ae6a Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Wed, 3 Apr 2024 19:18:01 +0900 Subject: [PATCH 5/6] Add test for protobuf scrape --- ...PrometheusScrapeEndpointIntegrationTests.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java index de7aaa643b57..6ea796ef3b5a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java @@ -21,6 +21,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; import io.prometheus.metrics.model.registry.PrometheusRegistry; @@ -120,6 +121,21 @@ void scrapeWithIncludedNames(WebTestClient client) { .doesNotContain("counter3_total")); } + @WebEndpointTest + void scrapeCanProducePrometheusProtobuf(WebTestClient client) { + MediaType prometheusProtobuf = MediaType.parseMediaType(PrometheusProtobufWriter.CONTENT_TYPE); + client.get() + .uri("/actuator/prometheus") + .accept(prometheusProtobuf) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(prometheusProtobuf) + .expectBody(byte[].class) + .value((body) -> assertThat(body).isNotEmpty()); + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { From 63a3d2a4ee413f8e24faa25735d284029057f19c Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:03:04 +0900 Subject: [PATCH 6/6] Upgrade client to 1.2.1, prometheusProperties, merge config props --- .../prometheus/PrometheusProperties.java | 156 +++++++++++++ .../PrometheusPropertiesConfigAdapter.java | 18 ++ ...eclientMetricsExportAutoConfiguration.java | 11 +- .../PrometheusSimpleclientProperties.java | 213 ------------------ ...usSimpleclientPropertiesConfigAdapter.java | 22 +- .../prometheus/PrometheusPropertiesTests.java | 11 + ...pleclientPropertiesConfigAdapterTests.java | 13 +- ...PrometheusSimpleclientPropertiesTests.java | 41 ---- ...etheusExemplarsAutoConfigurationTests.java | 74 +++--- .../spring-boot-dependencies/build.gradle | 2 +- 10 files changed, 250 insertions(+), 311 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java 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 8002db73303a..1e73c6ab7638 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 @@ -17,7 +17,10 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -42,6 +45,23 @@ public class PrometheusProperties { */ private boolean descriptions = true; + /** + * Configuration options for using Prometheus Pushgateway, allowing metrics to be + * pushed when they cannot be scraped. + */ + private final Pushgateway pushgateway = new Pushgateway(); + + /** + * Histogram type for backing DistributionSummary and Timer. + */ + @Deprecated(since = "3.3.0") + private HistogramFlavor histogramFlavor = HistogramFlavor.Prometheus; + + /** + * Additional properties to pass to the Prometheus client. + */ + private final Map prometheusProperties = new HashMap<>(); + /** * Step size (i.e. reporting frequency) to use. */ @@ -55,6 +75,14 @@ public void setDescriptions(boolean descriptions) { this.descriptions = descriptions; } + public HistogramFlavor getHistogramFlavor() { + return this.histogramFlavor; + } + + public void setHistogramFlavor(HistogramFlavor histogramFlavor) { + this.histogramFlavor = histogramFlavor; + } + public Duration getStep() { return this.step; } @@ -71,4 +99,132 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public Pushgateway getPushgateway() { + return this.pushgateway; + } + + public Map getPrometheusProperties() { + return this.prometheusProperties; + } + + /** + * Configuration options for push-based interaction with Prometheus. + */ + public static class Pushgateway { + + /** + * Enable publishing over a Prometheus Pushgateway. + */ + private Boolean enabled = false; + + /** + * Base URL for the Pushgateway. + */ + private String baseUrl = "http://localhost:9091"; + + /** + * Login user of the Prometheus Pushgateway. + */ + private String username; + + /** + * Login password of the Prometheus Pushgateway. + */ + private String password; + + /** + * Frequency with which to push metrics. + */ + private Duration pushRate = Duration.ofMinutes(1); + + /** + * Job identifier for this application instance. + */ + private String job; + + /** + * Grouping key for the pushed metrics. + */ + private Map groupingKey = new HashMap<>(); + + /** + * Operation that should be performed on shutdown. + */ + private ShutdownOperation shutdownOperation = ShutdownOperation.NONE; + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Duration getPushRate() { + return this.pushRate; + } + + public void setPushRate(Duration pushRate) { + this.pushRate = pushRate; + } + + public String getJob() { + return this.job; + } + + public void setJob(String job) { + this.job = job; + } + + public Map getGroupingKey() { + return this.groupingKey; + } + + public void setGroupingKey(Map groupingKey) { + this.groupingKey = groupingKey; + } + + public ShutdownOperation getShutdownOperation() { + return this.shutdownOperation; + } + + public void setShutdownOperation(ShutdownOperation shutdownOperation) { + this.shutdownOperation = shutdownOperation; + } + + } + + public enum HistogramFlavor { + + Prometheus, VictoriaMetrics; + + HistogramFlavor() { + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java index 8ec42c009e6f..e9101485a382 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; import java.time.Duration; +import java.util.Map; +import java.util.Properties; import io.micrometer.prometheusmetrics.PrometheusConfig; @@ -55,4 +57,20 @@ public Duration step() { return get(PrometheusProperties::getStep, PrometheusConfig.super::step); } + @Override + public Properties prometheusProperties() { + return get(this::fromPropertiesMap, PrometheusConfig.super::prometheusProperties); + } + + private Properties fromPropertiesMap(PrometheusProperties prometheusProperties) { + Map map = prometheusProperties.getPrometheusProperties(); + if (map.isEmpty()) { + return null; + } + Properties properties = PrometheusConfig.super.prometheusProperties(); + properties = (properties != null) ? properties : new Properties(); + properties.putAll(map); + return properties; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java index cf96499b56fd..1793369d4bb1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java @@ -71,12 +71,12 @@ @ConditionalOnBean(Clock.class) @ConditionalOnClass(PrometheusMeterRegistry.class) @ConditionalOnEnabledMetricsExport("prometheus") -@EnableConfigurationProperties(PrometheusSimpleclientProperties.class) +@EnableConfigurationProperties(PrometheusProperties.class) public class PrometheusSimpleclientMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - public PrometheusConfig simpleclientPrometheusConfig(PrometheusSimpleclientProperties prometheusProperties) { + public PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) { return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties); } @@ -134,9 +134,8 @@ public static class PrometheusPushGatewayConfiguration { @Bean @ConditionalOnMissingBean public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, - PrometheusSimpleclientProperties prometheusProperties, Environment environment) - throws MalformedURLException { - PrometheusSimpleclientProperties.Pushgateway properties = prometheusProperties.getPushgateway(); + PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { + PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); Duration pushRate = properties.getPushRate(); String job = getJob(properties, environment); Map groupingKey = properties.getGroupingKey(); @@ -154,7 +153,7 @@ private PushGateway initializePushGateway(String url) throws MalformedURLExcepti return new PushGateway(new URL(url)); } - private String getJob(PrometheusSimpleclientProperties.Pushgateway properties, Environment environment) { + 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/PrometheusSimpleclientProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java deleted file mode 100644 index d7772410198a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientProperties.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; - -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * {@link ConfigurationProperties @ConfigurationProperties} for configuring metrics export - * to Prometheus. - * - * @author Jon Schneider - * @author Stephane Nicoll - * @since 2.0.0 - * @deprecated use {@link PrometheusProperties} instead - */ -@Deprecated(forRemoval = true, since = "3.3.0") -@ConfigurationProperties(prefix = "management.prometheus.metrics.export") -public class PrometheusSimpleclientProperties { - - /** - * Whether exporting of metrics to this backend is enabled. - */ - private boolean enabled = true; - - /** - * Whether to enable publishing descriptions as part of the scrape payload to - * Prometheus. Turn this off to minimize the amount of data sent on each scrape. - */ - private boolean descriptions = true; - - /** - * Configuration options for using Prometheus Pushgateway, allowing metrics to be - * pushed when they cannot be scraped. - */ - private final Pushgateway pushgateway = new Pushgateway(); - - /** - * Histogram type for backing DistributionSummary and Timer. - */ - private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus; - - /** - * Step size (i.e. reporting frequency) to use. - */ - private Duration step = Duration.ofMinutes(1); - - public boolean isDescriptions() { - return this.descriptions; - } - - public void setDescriptions(boolean descriptions) { - this.descriptions = descriptions; - } - - public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { - return this.histogramFlavor; - } - - public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { - this.histogramFlavor = histogramFlavor; - } - - public Duration getStep() { - return this.step; - } - - public void setStep(Duration step) { - this.step = step; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Pushgateway getPushgateway() { - return this.pushgateway; - } - - /** - * Configuration options for push-based interaction with Prometheus. - */ - public static class Pushgateway { - - /** - * Enable publishing over a Prometheus Pushgateway. - */ - private Boolean enabled = false; - - /** - * Base URL for the Pushgateway. - */ - private String baseUrl = "http://localhost:9091"; - - /** - * Login user of the Prometheus Pushgateway. - */ - private String username; - - /** - * Login password of the Prometheus Pushgateway. - */ - private String password; - - /** - * Frequency with which to push metrics. - */ - private Duration pushRate = Duration.ofMinutes(1); - - /** - * Job identifier for this application instance. - */ - private String job; - - /** - * Grouping key for the pushed metrics. - */ - private Map groupingKey = new HashMap<>(); - - /** - * Operation that should be performed on shutdown. - */ - private ShutdownOperation shutdownOperation = ShutdownOperation.NONE; - - public Boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getPushRate() { - return this.pushRate; - } - - public void setPushRate(Duration pushRate) { - this.pushRate = pushRate; - } - - public String getJob() { - return this.job; - } - - public void setJob(String job) { - this.job = job; - } - - public Map getGroupingKey() { - return this.groupingKey; - } - - public void setGroupingKey(Map groupingKey) { - this.groupingKey = groupingKey; - } - - public ShutdownOperation getShutdownOperation() { - return this.shutdownOperation; - } - - public void setShutdownOperation(ShutdownOperation shutdownOperation) { - this.shutdownOperation = shutdownOperation; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java index eb889c5464ac..3b34752560e1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java @@ -18,20 +18,21 @@ import java.time.Duration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusProperties.HistogramFlavor; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; /** - * Adapter to convert {@link PrometheusSimpleclientProperties} to a + * Adapter to convert {@link PrometheusProperties} to a * {@link io.micrometer.prometheus.PrometheusConfig}. * * @author Jon Schneider * @author Phillip Webb */ @SuppressWarnings({ "deprecation", "removal" }) -class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter +class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter implements io.micrometer.prometheus.PrometheusConfig { - PrometheusSimpleclientPropertiesConfigAdapter(PrometheusSimpleclientProperties properties) { + PrometheusSimpleclientPropertiesConfigAdapter(PrometheusProperties properties) { super(properties); } @@ -47,19 +48,26 @@ public String get(String key) { @Override public boolean descriptions() { - return get(PrometheusSimpleclientProperties::isDescriptions, - io.micrometer.prometheus.PrometheusConfig.super::descriptions); + return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions); } @Override public io.micrometer.prometheus.HistogramFlavor histogramFlavor() { - return get(PrometheusSimpleclientProperties::getHistogramFlavor, + return get(PrometheusSimpleclientPropertiesConfigAdapter::mapToMicrometerHistogramFlavor, io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); } + static io.micrometer.prometheus.HistogramFlavor mapToMicrometerHistogramFlavor(PrometheusProperties properties) { + HistogramFlavor histogramFlavor = properties.getHistogramFlavor(); + return switch (histogramFlavor) { + case Prometheus -> io.micrometer.prometheus.HistogramFlavor.Prometheus; + case VictoriaMetrics -> io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics; + }; + } + @Override public Duration step() { - return get(PrometheusSimpleclientProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java index f41c8ede9b49..cfdd0a4188c8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java @@ -35,4 +35,15 @@ void defaultValuesAreConsistent() { assertThat(properties.getStep()).isEqualTo(config.step()); } + @SuppressWarnings("deprecation") + @Test + void defaultValuesAreConsistentWithSimpleclient() { + PrometheusProperties properties = new PrometheusProperties(); + io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT; + assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); + assertThat(PrometheusSimpleclientPropertiesConfigAdapter.mapToMicrometerHistogramFlavor(properties)) + .isEqualTo(config.histogramFlavor()); + assertThat(properties.getStep()).isEqualTo(config.step()); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java index 0b6dbea932a5..56edc2f62bc7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusProperties.HistogramFlavor; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.AbstractPropertiesConfigAdapterTests; import static org.assertj.core.api.Assertions.assertThat; @@ -29,9 +30,9 @@ * * @author Mirko Sobeck */ -@SuppressWarnings({ "deprecation", "removal" }) +@SuppressWarnings({ "deprecation" }) class PrometheusSimpleclientPropertiesConfigAdapterTests extends - AbstractPropertiesConfigAdapterTests { + AbstractPropertiesConfigAdapterTests { PrometheusSimpleclientPropertiesConfigAdapterTests() { super(PrometheusSimpleclientPropertiesConfigAdapter.class); @@ -39,22 +40,22 @@ class PrometheusSimpleclientPropertiesConfigAdapterTests extends @Test void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { - PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); + PrometheusProperties properties = new PrometheusProperties(); properties.setDescriptions(false); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).descriptions()).isFalse(); } @Test void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { - PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); - properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); + PrometheusProperties properties = new PrometheusProperties(); + properties.setHistogramFlavor(HistogramFlavor.VictoriaMetrics); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).histogramFlavor()) .isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); } @Test void whenPropertiesStepIsSetAdapterStepReturnsIt() { - PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); + PrometheusProperties properties = new PrometheusProperties(); properties.setStep(Duration.ofSeconds(30)); assertThat(new PrometheusSimpleclientPropertiesConfigAdapter(properties).step()) .isEqualTo(Duration.ofSeconds(30)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java deleted file mode 100644 index 3eb38ebb327f..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesTests.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2024 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.autoconfigure.metrics.export.prometheus; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PrometheusSimpleclientProperties}. - * - * @author Stephane Nicoll - */ -@SuppressWarnings("removal") -class PrometheusSimpleclientPropertiesTests { - - @SuppressWarnings("deprecation") - @Test - void defaultValuesAreConsistentWithSimpleclient() { - PrometheusSimpleclientProperties properties = new PrometheusSimpleclientProperties(); - io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT; - assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); - assertThat(properties.getHistogramFlavor()).isEqualTo(config.histogramFlavor()); - assertThat(properties.getStep()).isEqualTo(config.step()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index 6aae14d135cf..55a499dde8e3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -83,6 +83,33 @@ void shouldSupplyCustomBeans() { .isSameAs(CustomConfiguration.SUPPLIER)); } + @Test + void prometheusOpenMetricsOutputWithoutExemplarsOnHistogramCount() { + this.contextRunner.withPropertyValues( + "management.prometheus.metrics.export.prometheus-properties.io.prometheus.exporter.exemplarsOnAllMetricTypes=false") + .run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(1); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(1); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty(); + }); + } + @Test void prometheusOpenMetricsOutputShouldContainExemplars() { this.contextRunner.run((context) -> { @@ -94,8 +121,8 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(1); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(1); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); Optional bucketTraceInfo = openMetricsOutput.lines() .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) @@ -104,42 +131,15 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) .findFirst(); - assertThat(bucketTraceInfo).isNotEmpty(); - }); - } - - @Test - void prometheusOpenMetricsOutputCanBeConfiguredToContainExemplarsOnHistogramCount() { - this.contextRunner.withSystemProperties("io.prometheus.exporter.exemplarsOnAllMetricTypes=true") - .run((context) -> { - assertThat(context).hasSingleBean(SpanContext.class); - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test.observation", observationRegistry).stop(); - PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); - String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); - System.out.println(openMetricsOutput); - - assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); - assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); - assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); - - Optional bucketTraceInfo = openMetricsOutput.lines() - .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) - .map(BUCKET_TRACE_INFO_PATTERN::matcher) - .flatMap(Matcher::results) - .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) - .findFirst(); - - Optional counterTraceInfo = openMetricsOutput.lines() - .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) - .map(COUNT_TRACE_INFO_PATTERN::matcher) - .flatMap(Matcher::results) - .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) - .findFirst(); + Optional counterTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) + .map(COUNT_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); - assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); - }); + assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); + }); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index baec22c1d7a4..15e5daa15bae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1415,7 +1415,7 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "1.2.0") { + library("Prometheus Client", "1.2.1") { group("io.prometheus") { imports = [ "prometheus-metrics-bom"