diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index dcb60863a2..277ee3a325 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -53,6 +53,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" @@ -141,6 +142,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 2be7ec73bf..9aba0ae77e 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,18 +16,11 @@ 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 io.prometheus.metrics.tracer.common.SpanContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; @@ -35,20 +28,16 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegi import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; -import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager; -import org.springframework.boot.actuate.metrics.export.prometheus.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 +47,43 @@ import org.springframework.util.StringUtils; * @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) { + 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()); + PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig, + PrometheusRegistry prometheusRegistry, Clock clock, ObjectProvider spanContext) { + return new PrometheusMeterRegistry(prometheusConfig, prometheusRegistry, clock, spanContext.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); + PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); } @Configuration(proxyBeanMethods = false) @ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint.class) - public static class PrometheusScrapeEndpointConfiguration { + 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 }) + 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 c22be89e7d..ea5991ad4d 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 @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring metrics export @@ -31,7 +32,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Stephane Nicoll * @since 2.0.0 */ -@SuppressWarnings("deprecation") @ConfigurationProperties(prefix = "management.prometheus.metrics.export") public class PrometheusProperties { @@ -55,7 +55,13 @@ public class PrometheusProperties { /** * Histogram type for backing DistributionSummary and Timer. */ - private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus; + @Deprecated(since = "3.3.0", forRemoval = true) + private HistogramFlavor histogramFlavor = HistogramFlavor.Prometheus; + + /** + * Additional properties to pass to the Prometheus client. + */ + private final Map properties = new HashMap<>(); /** * Step size (i.e. reporting frequency) to use. @@ -70,11 +76,14 @@ public class PrometheusProperties { this.descriptions = descriptions; } - public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { + @Deprecated(since = "3.3.0", forRemoval = true) + @DeprecatedConfigurationProperty(since = "3.3.0", + reason = "No longer supported. Works only when using the Prometheus simpleclient.") + public HistogramFlavor getHistogramFlavor() { return this.histogramFlavor; } - public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { + public void setHistogramFlavor(HistogramFlavor histogramFlavor) { this.histogramFlavor = histogramFlavor; } @@ -98,6 +107,10 @@ public class PrometheusProperties { return this.pushgateway; } + public Map getProperties() { + return this.properties; + } + /** * Configuration options for push-based interaction with Prometheus. */ @@ -209,4 +222,16 @@ public class PrometheusProperties { } + /** + * Prometheus Histogram flavor. + * + * @deprecated since 3.3.0 for removal in 3.5.0 + */ + @Deprecated(since = "3.3.0", forRemoval = true) + public enum HistogramFlavor { + + Prometheus, VictoriaMetrics; + + } + } 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 47b50a7bca..265e5755cc 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,19 +17,21 @@ 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; 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 +49,28 @@ class PrometheusPropertiesConfigAdapter extends PropertiesConfigAdapter map = prometheusProperties.getProperties(); + 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 new file mode 100644 index 0000000000..371de66e43 --- /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,164 @@ +/* + * 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 since 3.3.0 for removal in 3.5.0 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 + PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) { + return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties); + } + + @Bean + @ConditionalOnMissingBean + 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 + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + @ConditionalOnMissingBean(ExemplarSampler.class) + @ConditionalOnBean(SpanContextSupplier.class) + DefaultExemplarSampler exemplarSampler(SpanContextSupplier spanContextSupplier) { + return new DefaultExemplarSampler(spanContextSupplier); + } + + @SuppressWarnings("removal") + @Configuration(proxyBeanMethods = false) + @ConditionalOnAvailableEndpoint(endpoint = PrometheusSimpleclientScrapeEndpoint.class) + static class PrometheusScrapeEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean({ PrometheusSimpleclientScrapeEndpoint.class, PrometheusScrapeEndpoint.class }) + 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") + static class PrometheusPushGatewayConfiguration { + + /** + * The fallback job name. We use 'spring' since there's a history of Prometheus + * spring integration defaulting to that name from when Prometheus integration + * didn't exist in Spring itself. + */ + private static final String FALLBACK_JOB = "spring"; + + @Bean + @ConditionalOnMissingBean + PrometheusPushGatewayManager prometheusPushGatewayManager(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 0000000000..d685f1bdcf --- /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,71 @@ +/* + * 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", "removal" }) +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(PrometheusSimpleclientPropertiesConfigAdapter::mapToMicrometerHistogramFlavor, + io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); + } + + static io.micrometer.prometheus.HistogramFlavor mapToMicrometerHistogramFlavor(PrometheusProperties properties) { + return switch (properties.getHistogramFlavor()) { + case Prometheus -> io.micrometer.prometheus.HistogramFlavor.Prometheus; + case VictoriaMetrics -> io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics; + }; + } + + @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 4176d2c246..54bc3fa255 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,7 +18,7 @@ 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 io.prometheus.metrics.tracer.common.SpanContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; @@ -41,42 +41,42 @@ import org.springframework.util.function.SingletonSupplier; @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; @@ -85,6 +85,10 @@ public class PrometheusExemplarsAutoConfiguration { 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 0000000000..12f77e31f2 --- /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,97 @@ +/* + * 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 + * @deprecated since 3.3.0 for removal in 3.5.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/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 023583ac5a..d3a6fc7ead 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 @@ -109,6 +110,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 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 d51980437b..429d2e34d4 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 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentatio 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 @@ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocument 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 0000000000..991a36e1db --- /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 5b209ffda4..7f0783acd3 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 @@ package org.springframework.boot.actuate.autoconfigure.metrics; 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 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Jon Schneider * @author Andy Wilkinson */ -@SuppressWarnings("deprecation") class MeterRegistryCustomizerTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -63,7 +63,7 @@ class MeterRegistryCustomizerTests { @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 @@ class MeterRegistryCustomizerTests { } @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 0000000000..9b607d834e --- /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 e691fc1897..319ee2393f 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,33 +17,22 @@ 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 io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.tracer.common.SpanContext; 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.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.FilteredClassLoader; 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}. @@ -52,25 +41,25 @@ import static org.mockito.Mockito.mock; * @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 +67,9 @@ class PrometheusMetricsExportAutoConfigurationTests { 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,61 +77,46 @@ class PrometheusMetricsExportAutoConfigurationTests { 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 - void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + void autoConfiguresPrometheusMeterRegistryIfSpanContextIsPresent() { 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)); + .run((context) -> assertThat(context).hasSingleBean(SpanContext.class) + .hasSingleBean(PrometheusMeterRegistry.class)); } @Test @@ -183,65 +157,6 @@ class PrometheusMetricsExportAutoConfigurationTests { .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 { @@ -257,7 +172,7 @@ class PrometheusMetricsExportAutoConfigurationTests { static class CustomConfigConfiguration { @Bean - io.micrometer.prometheus.PrometheusConfig customConfig() { + io.micrometer.prometheusmetrics.PrometheusConfig customConfig() { return (key) -> null; } @@ -268,20 +183,21 @@ class PrometheusMetricsExportAutoConfigurationTests { 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 +207,8 @@ class PrometheusMetricsExportAutoConfigurationTests { static class CustomEndpointConfiguration { @Bean - PrometheusScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); + PrometheusScrapeEndpoint customEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } } @@ -302,24 +218,27 @@ class PrometheusMetricsExportAutoConfigurationTests { 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/PrometheusPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java index f38efdcbc0..09f34b55a8 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 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Mirko Sobeck */ -@SuppressWarnings("deprecation") class PrometheusPropertiesConfigAdapterTests extends AbstractPropertiesConfigAdapterTests { @@ -44,14 +43,6 @@ class PrometheusPropertiesConfigAdapterTests 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 b2297f9735..cfdd0a4188 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,15 +25,24 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Stephane Nicoll */ -@SuppressWarnings("deprecation") class PrometheusPropertiesTests { @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()); + } + + @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(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/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 0000000000..d409db5f7b --- /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 0000000000..cfd4701d6d --- /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,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.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; + +/** + * Tests for {@link PrometheusSimpleclientPropertiesConfigAdapter}. + * + * @author Mirko Sobeck + */ +@SuppressWarnings({ "deprecation", "removal" }) +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(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/LazyTracingSpanContextSupplierTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java index 2cc20b2ebb..fde7e4d628 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.junit.jupiter.api.Test; 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 @@ class LazyTracingSpanContextSupplierTests { }; - 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 0000000000..ffee8531bb --- /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 bc6030fdf2..4777a0c9d5 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,8 +22,9 @@ 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 io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.tracer.common.SpanContext; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; @@ -49,10 +50,10 @@ import static org.mockito.Mockito.mock; 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", @@ -64,34 +65,59 @@ class PrometheusExemplarsAutoConfigurationTests { @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 prometheusOpenMetricsOutputWithoutExemplarsOnHistogramCount() { + this.contextRunner.withPropertyValues( + "management.prometheus.metrics.export.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) -> { - 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"); @@ -107,7 +133,7 @@ class PrometheusExemplarsAutoConfigurationTests { Optional counterTraceInfo = openMetricsOutput.lines() .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) - .map(COUNTER_TRACE_INFO_PATTERN::matcher) + .map(COUNT_TRACE_INFO_PATTERN::matcher) .flatMap(Matcher::results) .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) .findFirst(); @@ -119,10 +145,10 @@ class PrometheusExemplarsAutoConfigurationTests { @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 0000000000..b16f687532 --- /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-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 023eb3c1bd..98caa5d11f 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 0000000000..25d18012e7 --- /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 9f0a1db9bd..19bad830c5 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; @@ -37,6 +35,7 @@ import org.springframework.lang.Nullable; * * @author Jon Schneider * @author Johnny Lim + * @author Moritz Halbritter * @since 2.0.0 */ @WebEndpoint(id = "prometheus") @@ -44,30 +43,26 @@ 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); - - String scrapePage = writer.toString(); - this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; - - return new WebEndpointResponse<>(scrapePage, format); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize); + MetricSnapshots metricSnapshots = (includedNames != null) + ? this.prometheusRegistry.scrape(includedNames::contains) : this.prometheusRegistry.scrape(); + format.write(outputStream, metricSnapshots); + byte[] content = outputStream.toByteArray(); + this.nextMetricsScrapeSize = content.length + METRICS_SCRAPE_CHARS_EXTRA; + return new WebEndpointResponse<>(content, 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 0000000000..7e81405cdd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java @@ -0,0 +1,77 @@ +/* + * 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 since 3.3.0 for removal in 3.5.0 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 54b16b7110..659893d8f2 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 @@ import org.springframework.util.MimeTypeUtils; * * @author Andy Wilkinson * @since 2.5.0 + * @deprecated since 3.3.0 for removal in 3.5.0 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 49c3a658db..6ea796ef3b 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,11 @@ 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 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; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.context.annotation.Bean; @@ -40,8 +43,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 +59,8 @@ class PrometheusScrapeEndpointIntegrationTests { @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 +77,7 @@ class PrometheusScrapeEndpointIntegrationTests { @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 +94,8 @@ class PrometheusScrapeEndpointIntegrationTests { @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,36 +109,50 @@ class PrometheusScrapeEndpointIntegrationTests { @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") .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 { @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 0000000000..94766f193e --- /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 0000000000..b77c73f6e4 --- /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 316e0e4045..7fe68aa2c2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1241,7 +1241,7 @@ bom { ] } } - library("Micrometer", "1.13.0-M2") { + library("Micrometer", "1.13.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ @@ -1424,7 +1424,18 @@ bom { releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}") } } - library("Prometheus Client", "0.16.0") { + library("Prometheus Client", "1.2.1") { + group("io.prometheus") { + imports = [ + "prometheus-metrics-bom" + ] + } + 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" diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 06a39a25bf..b03123f91d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -543,11 +543,13 @@ scrape_configs: ---- https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Exemplars] are also supported. -To enable this feature, a `SpanContextSupplier` bean should be present. +To enable this feature, a `SpanContext` bean should be present. +If you're using the deprecated Prometheus simpleclient support and want to enable that feature, a `SpanContextSupplier` bean should be present. If you use https://micrometer.io/docs/tracing[Micrometer Tracing], this will be auto-configured for you, but you can always create your own if you want. Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Docs], since this feature needs to be explicitly enabled on Prometheus' side, and it is only supported using the https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#exemplars[OpenMetrics] format. For ephemeral or batch jobs that may not exist long enough to be scraped, you can use https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support to expose the metrics to Prometheus. +The Prometheus Pushgateway only works with the deprecated Prometheus simpleclient for now, until the Prometheus client 1.x adds support for it. To enable Prometheus Pushgateway support, add the following dependency to your project: [source,xml]