diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a3cf3a70fda3..8a31a14a7dc0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -56,6 +56,7 @@ dependencies { optional("io.micrometer:micrometer-registry-kairos") optional("io.micrometer:micrometer-registry-new-relic") optional("io.micrometer:micrometer-registry-otlp") + optional("io.micrometer:micrometer-registry-prometheus") optional("io.micrometer:micrometer-registry-prometheus-simpleclient") optional("io.micrometer:micrometer-registry-stackdriver") { exclude group: "commons-logging", module: "commons-logging" @@ -144,6 +145,7 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("io.micrometer:micrometer-observation-test") testImplementation("io.projectreactor:reactor-test") + testImplementation("io.prometheus:prometheus-metrics-exposition-formats") testImplementation("io.r2dbc:r2dbc-h2") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.jayway.jsonpath:json-path") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java index 2be7ec73bf49..c876428d2221 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java @@ -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.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 @@ * @author Jonatan Ivanov * @since 2.0.0 */ -@SuppressWarnings("deprecation") @AutoConfiguration( before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }, after = MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) -@ConditionalOnClass(io.micrometer.prometheus.PrometheusMeterRegistry.class) +@ConditionalOnClass(PrometheusMeterRegistry.class) @ConditionalOnEnabledMetricsExport("prometheus") @EnableConfigurationProperties(PrometheusProperties.class) public class PrometheusMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - public io.micrometer.prometheus.PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperties) { + public PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperties) { return new PrometheusPropertiesConfigAdapter(prometheusProperties); } @Bean @ConditionalOnMissingBean - public io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry( - io.micrometer.prometheus.PrometheusConfig prometheusConfig, CollectorRegistry collectorRegistry, - Clock clock, ObjectProvider exemplarSamplerProvider) { - return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock, - exemplarSamplerProvider.getIfAvailable()); + public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig, + PrometheusRegistry prometheusRegistry, Clock clock, 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); + public PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); } @Configuration(proxyBeanMethods = false) @ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint.class) public static class PrometheusScrapeEndpointConfiguration { + @SuppressWarnings("removal") @Bean - @ConditionalOnMissingBean - public PrometheusScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); - } - - } - - /** - * Configuration for Prometheus - * Pushgateway. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(PushGateway.class) - @ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled") - public static class PrometheusPushGatewayConfiguration { - - /** - * The fallback job name. We use 'spring' since there's a history of Prometheus - * spring integration defaulting to that name from when Prometheus integration - * didn't exist in Spring itself. - */ - private static final String FALLBACK_JOB = "spring"; - - @Bean - @ConditionalOnMissingBean - public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, - PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { - PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); - Duration pushRate = properties.getPushRate(); - String job = getJob(properties, environment); - Map groupingKey = properties.getGroupingKey(); - ShutdownOperation shutdownOperation = properties.getShutdownOperation(); - PushGateway pushGateway = initializePushGateway(properties.getBaseUrl()); - if (StringUtils.hasText(properties.getUsername())) { - pushGateway.setConnectionFactory( - new BasicAuthHttpConnectionFactory(properties.getUsername(), properties.getPassword())); - } - return new PrometheusPushGatewayManager(pushGateway, collectorRegistry, pushRate, job, groupingKey, - shutdownOperation); - } - - private PushGateway initializePushGateway(String url) throws MalformedURLException { - return new PushGateway(new URL(url)); - } - - private String getJob(PrometheusProperties.Pushgateway properties, Environment environment) { - String job = properties.getJob(); - job = (job != null) ? job : environment.getProperty("spring.application.name"); - return (job != null) ? job : FALLBACK_JOB; + @ConditionalOnMissingBean({ PrometheusScrapeEndpoint.class, PrometheusSimpleclientScrapeEndpoint.class }) + public PrometheusScrapeEndpoint prometheusEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java index c22be89e7d3c..1e73c6ab7638 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java @@ -31,7 +31,6 @@ * @author Stephane Nicoll * @since 2.0.0 */ -@SuppressWarnings("deprecation") @ConfigurationProperties(prefix = "management.prometheus.metrics.export") public class PrometheusProperties { @@ -55,7 +54,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") + private HistogramFlavor histogramFlavor = HistogramFlavor.Prometheus; + + /** + * Additional properties to pass to the Prometheus client. + */ + private final Map prometheusProperties = new HashMap<>(); /** * Step size (i.e. reporting frequency) to use. @@ -70,11 +75,11 @@ public void setDescriptions(boolean descriptions) { this.descriptions = descriptions; } - public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() { + public HistogramFlavor getHistogramFlavor() { return this.histogramFlavor; } - public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) { + public void setHistogramFlavor(HistogramFlavor histogramFlavor) { this.histogramFlavor = histogramFlavor; } @@ -98,6 +103,10 @@ public Pushgateway getPushgateway() { return this.pushgateway; } + public Map getPrometheusProperties() { + return this.prometheusProperties; + } + /** * Configuration options for push-based interaction with Prometheus. */ @@ -209,4 +218,13 @@ public void setShutdownOperation(ShutdownOperation shutdownOperation) { } + public enum HistogramFlavor { + + Prometheus, VictoriaMetrics; + + HistogramFlavor() { + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java index 47b50a7bca2b..e9101485a382 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java @@ -17,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 @@ public String get(String key) { @Override public boolean descriptions() { - return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions); + return get(PrometheusProperties::isDescriptions, PrometheusConfig.super::descriptions); } @Override - public io.micrometer.prometheus.HistogramFlavor histogramFlavor() { - return get(PrometheusProperties::getHistogramFlavor, - io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor); + public Duration step() { + return get(PrometheusProperties::getStep, PrometheusConfig.super::step); } @Override - public Duration step() { - return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + public Properties prometheusProperties() { + return get(this::fromPropertiesMap, PrometheusConfig.super::prometheusProperties); + } + + private Properties fromPropertiesMap(PrometheusProperties prometheusProperties) { + Map map = prometheusProperties.getPrometheusProperties(); + if (map.isEmpty()) { + return null; + } + Properties properties = PrometheusConfig.super.prometheusProperties(); + properties = (properties != null) ? properties : new Properties(); + properties.putAll(map); + return properties; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfiguration.java new file mode 100644 index 000000000000..1793369d4bb1 --- /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 in favor of {@link PrometheusMetricsExportAutoConfiguration} + */ +@SuppressWarnings("removal") +@Deprecated(since = "3.3.0", forRemoval = true) +@AutoConfiguration( + before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }, + after = { MetricsAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class }) +@ConditionalOnBean(Clock.class) +@ConditionalOnClass(PrometheusMeterRegistry.class) +@ConditionalOnEnabledMetricsExport("prometheus") +@EnableConfigurationProperties(PrometheusProperties.class) +public class PrometheusSimpleclientMetricsExportAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) { + return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties); + } + + @Bean + @ConditionalOnMissingBean + public io.micrometer.prometheus.PrometheusMeterRegistry simpleclientPrometheusMeterRegistry( + io.micrometer.prometheus.PrometheusConfig prometheusConfig, CollectorRegistry collectorRegistry, + Clock clock, ObjectProvider exemplarSamplerProvider) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock, + exemplarSamplerProvider.getIfAvailable()); + } + + @Bean + @ConditionalOnMissingBean + public CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + @ConditionalOnMissingBean(ExemplarSampler.class) + @ConditionalOnBean(SpanContextSupplier.class) + public DefaultExemplarSampler exemplarSampler(SpanContextSupplier spanContextSupplier) { + return new DefaultExemplarSampler(spanContextSupplier); + } + + @SuppressWarnings("removal") + @Configuration(proxyBeanMethods = false) + @ConditionalOnAvailableEndpoint(endpoint = PrometheusSimpleclientScrapeEndpoint.class) + public static class PrometheusScrapeEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean({ PrometheusSimpleclientScrapeEndpoint.class, PrometheusScrapeEndpoint.class }) + public PrometheusSimpleclientScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + /** + * Configuration for Prometheus + * Pushgateway. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(PushGateway.class) + @ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled") + public static class PrometheusPushGatewayConfiguration { + + /** + * The fallback job name. We use 'spring' since there's a history of Prometheus + * spring integration defaulting to that name from when Prometheus integration + * didn't exist in Spring itself. + */ + private static final String FALLBACK_JOB = "spring"; + + @Bean + @ConditionalOnMissingBean + public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry, + PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException { + PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway(); + Duration pushRate = properties.getPushRate(); + String job = getJob(properties, environment); + Map groupingKey = properties.getGroupingKey(); + ShutdownOperation shutdownOperation = properties.getShutdownOperation(); + PushGateway pushGateway = initializePushGateway(properties.getBaseUrl()); + if (StringUtils.hasText(properties.getUsername())) { + pushGateway.setConnectionFactory( + new BasicAuthHttpConnectionFactory(properties.getUsername(), properties.getPassword())); + } + return new PrometheusPushGatewayManager(pushGateway, collectorRegistry, pushRate, job, groupingKey, + shutdownOperation); + } + + private PushGateway initializePushGateway(String url) throws MalformedURLException { + return new PushGateway(new URL(url)); + } + + private String getJob(PrometheusProperties.Pushgateway properties, Environment environment) { + String job = properties.getJob(); + job = (job != null) ? job : environment.getProperty("spring.application.name"); + return (job != null) ? job : FALLBACK_JOB; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapter.java new file mode 100644 index 000000000000..3b34752560e1 --- /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,73 @@ +/* + * 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.prometheus.PrometheusProperties.HistogramFlavor; +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) { + HistogramFlavor histogramFlavor = properties.getHistogramFlavor(); + return switch (histogramFlavor) { + case Prometheus -> io.micrometer.prometheus.HistogramFlavor.Prometheus; + case VictoriaMetrics -> io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics; + }; + } + + @Override + public Duration step() { + return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java index 4176d2c2462b..54bc3fa255b4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -18,7 +18,7 @@ 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 @@ @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 boolean isSampled() { return sampled != null && sampled; } + @Override + public void markCurrentSpanAsExemplar() { + } + private Span currentSpan() { return this.tracer.obtain().currentSpan(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java new file mode 100644 index 000000000000..5ce8bbe8f953 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfiguration.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.tracing.prometheus; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.util.function.SingletonSupplier; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Prometheus Exemplars with + * Micrometer Tracing. + * + * @author Jonatan Ivanov + * @since 3.0.0 + */ +@SuppressWarnings("removal") +@Deprecated(forRemoval = true, since = "3.3.0") +@AutoConfiguration(before = PrometheusSimpleclientMetricsExportAutoConfiguration.class, + after = MicrometerTracingAutoConfiguration.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) +public class PrometheusSimpleclientExemplarsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + SpanContextSupplier spanContextSupplier(ObjectProvider tracerProvider) { + return new LazyTracingSpanContextSupplier(tracerProvider); + } + + /** + * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the + * {@link Tracer} can depend on the MeterRegistry (recording metrics), this + * {@link SpanContextSupplier} breaks the cycle by lazily loading the {@link Tracer}. + */ + static class LazyTracingSpanContextSupplier implements SpanContextSupplier { + + private final SingletonSupplier tracer; + + LazyTracingSpanContextSupplier(ObjectProvider tracerProvider) { + this.tracer = SingletonSupplier.of(tracerProvider::getObject); + } + + @Override + public String getTraceId() { + Span currentSpan = currentSpan(); + return (currentSpan != null) ? currentSpan.context().traceId() : null; + } + + @Override + public String getSpanId() { + Span currentSpan = currentSpan(); + return (currentSpan != null) ? currentSpan.context().spanId() : null; + } + + @Override + public boolean isSampled() { + Span currentSpan = currentSpan(); + if (currentSpan == null) { + return false; + } + Boolean sampled = currentSpan.context().sampled(); + return sampled != null && sampled; + } + + private Span currentSpan() { + return this.tracer.obtain().currentSpan(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 7801946776fc..722368cd6277 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -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 @@ -108,6 +109,7 @@ org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfigurati org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java index d51980437b7e..429d2e34d4da 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java @@ -18,8 +18,9 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; -import io.prometheus.client.CollectorRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.client.exporter.common.TextFormat; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; @@ -72,13 +73,12 @@ void filteredPrometheus() throws Exception { static class TestConfiguration { @Bean - @SuppressWarnings("deprecation") PrometheusScrapeEndpoint endpoint() { - CollectorRegistry collectorRegistry = new CollectorRegistry(true); - io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( - (key) -> null, collectorRegistry, Clock.SYSTEM); + PrometheusRegistry prometheusRegistry = new PrometheusRegistry(); + PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((key) -> null, prometheusRegistry, + Clock.SYSTEM); new JvmMemoryMetrics().bindTo(meterRegistry); - return new PrometheusScrapeEndpoint(collectorRegistry); + return new PrometheusScrapeEndpoint(prometheusRegistry); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java new file mode 100644 index 000000000000..991a36e1dbb9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusSimpleclientScrapeEndpointDocumentationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for generating documentation describing the + * {@link PrometheusSimpleclientScrapeEndpoint}. + * + * @author Andy Wilkinson + * @author Johnny Lim + */ +class PrometheusSimpleclientScrapeEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + @Test + void prometheus() throws Exception { + this.mockMvc.perform(get("/actuator/prometheus")) + .andExpect(status().isOk()) + .andDo(document("prometheus-simpleclient/all")); + } + + @Test + void prometheusOpenmetrics() throws Exception { + this.mockMvc.perform(get("/actuator/prometheus").accept(TextFormat.CONTENT_TYPE_OPENMETRICS_100)) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "application/openmetrics-text;version=1.0.0;charset=utf-8")) + .andDo(document("prometheus-simpleclient/openmetrics")); + } + + @Test + void filteredPrometheus() throws Exception { + this.mockMvc + .perform(get("/actuator/prometheus").param("includedNames", + "jvm_memory_used_bytes,jvm_memory_committed_bytes")) + .andExpect(status().isOk()) + .andDo(document("prometheus-simpleclient/names", + queryParameters(parameterWithName("includedNames") + .description("Restricts the samples to those that match the names. Optional.") + .optional()))); + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + @SuppressWarnings({ "removal", "deprecation" }) + PrometheusSimpleclientScrapeEndpoint endpoint() { + CollectorRegistry collectorRegistry = new CollectorRegistry(true); + io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( + (key) -> null, collectorRegistry, Clock.SYSTEM); + new JvmMemoryMetrics().bindTo(meterRegistry); + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java index 5b209ffda463..7f0783acd373 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java @@ -18,6 +18,7 @@ import io.micrometer.atlas.AtlasMeterRegistry; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.export.atlas.AtlasMetricsExportAutoConfiguration; @@ -36,7 +37,6 @@ * @author Jon Schneider * @author Andy Wilkinson */ -@SuppressWarnings("deprecation") class MeterRegistryCustomizerTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -63,7 +63,7 @@ void commonTagsAreAppliedBeforeRegistryIsInjectableElsewhere() { @Test void customizersCanBeAppliedToSpecificRegistryTypes() { this.contextRunner.withUserConfiguration(MeterRegistryCustomizerConfiguration.class).run((context) -> { - MeterRegistry prometheus = context.getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class); + MeterRegistry prometheus = context.getBean(PrometheusMeterRegistry.class); prometheus.get("jvm.memory.used").tags("job", "myjob").gauge(); MeterRegistry atlas = context.getBean(AtlasMeterRegistry.class); assertThat(atlas.find("jvm.memory.used").tags("job", "myjob").gauge()).isNull(); @@ -79,7 +79,7 @@ MeterRegistryCustomizer commonTags() { } @Bean - MeterRegistryCustomizer prometheusOnlyCommonTags() { + MeterRegistryCustomizer prometheusOnlyCommonTags() { return (registry) -> registry.config().commonTags("job", "myjob"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..9b607d834e43 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/DualPrometheusMetricsExportAutoConfigurationTests.java @@ -0,0 +1,409 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exemplars.ExemplarSampler; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.DefaultHttpConnectionFactory; +import io.prometheus.client.exporter.HttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.DualPrometheusMetricsExportAutoConfigurationTests.CustomSecondEndpointConfiguration.SecondPrometheusScrapeEndpoint; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientMetricsExportAutoConfiguration} and + * {@link PrometheusMetricsExportAutoConfiguration} with both Prometheus clients on the + * classpath. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Jonatan Ivanov + */ +@SuppressWarnings({ "removal", "deprecation" }) +@ExtendWith(OutputCaptureExtension.class) +class DualPrometheusMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, + PrometheusMetricsExportAutoConfiguration.class)); + + @Test + void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void autoConfiguresItsConfigPrometheusRegistryAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.defaults.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.prometheus.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customConfig") + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) + .hasBean("otherCustomConfig")); + } + + @Test + void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) + .hasBean("otherCustomRegistry")); + } + + @Test + void allowsCustomCollectorRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customCollectorRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customPrometheusRegistry") + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); + } + + @Test + void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .hasSingleBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void allowsCustomExemplarSamplerToBeUsed() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) + .run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class) + .getBean(ExemplarSampler.class) + .isSameAs(context.getBean("customExemplarSampler"))); + } + + @Test + void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) + .doesNotHaveBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void addsScrapeEndpointToManagementContext() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .run((context) -> assertThat(context).hasSingleBean(PrometheusScrapeEndpoint.class) + .doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointNotAddedToManagementContextWhenNotExposed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class) + .doesNotHaveBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .withPropertyValues("management.endpoint.prometheus.enabled=false") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class) + .doesNotHaveBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void allowsCustomScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void allowsCustomSecondScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomSecondEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customSecondEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class) + .hasSingleBean(SecondPrometheusScrapeEndpoint.class) + .hasSingleBean(PrometheusScrapeEndpoint.class)); + } + + @Test + void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); + } + + @Test + void withPushGatewayEnabled(CapturedOutput output) { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> { + assertThat(output).doesNotContain("Invalid PushGateway base url"); + hasGatewayURL(context, "http://localhost:9091/metrics/"); + }); + } + + @Test + void withPushGatewayNoBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(DefaultHttpConnectionFactory.class))); + } + + @Test + void withCustomPushGatewayURL() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); + } + + @Test + void withPushGatewayBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.username=admin", + "management.prometheus.metrics.export.pushgateway.password=secret") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(BasicAuthHttpConnectionFactory.class))); + } + + private void hasGatewayURL(AssertableApplicationContext context, String url) { + assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + } + + private ContextConsumer hasHttpConnectionFactory( + ThrowingConsumer httpConnectionFactory) { + return (context) -> { + PushGateway pushGateway = getPushGateway(context); + httpConnectionFactory + .accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory")); + }; + } + + private PushGateway getPushGateway(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class); + PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class); + return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway"); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusConfig customConfig() { + return (key) -> null; + } + + @Bean + io.micrometer.prometheusmetrics.PrometheusConfig otherCustomConfig() { + return (key) -> null; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + } + + @Bean + io.micrometer.prometheusmetrics.PrometheusMeterRegistry otherCustomRegistry( + io.micrometer.prometheusmetrics.PrometheusConfig config, PrometheusRegistry prometheusRegistry, + Clock clock) { + return new io.micrometer.prometheusmetrics.PrometheusMeterRegistry(config, prometheusRegistry, clock); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomCollectorRegistryConfiguration { + + @Bean + CollectorRegistry customCollectorRegistry() { + return new CollectorRegistry(); + } + + @Bean + PrometheusRegistry customPrometheusRegistry() { + return new PrometheusRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomEndpointConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomSecondEndpointConfiguration { + + @Bean + PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); + } + + @Bean + SecondPrometheusScrapeEndpoint customSecondEndpoint(CollectorRegistry collectorRegistry) { + return new SecondPrometheusScrapeEndpoint(collectorRegistry); + } + + @WebEndpoint(id = "prometheussc") + static class SecondPrometheusScrapeEndpoint extends PrometheusSimpleclientScrapeEndpoint { + + SecondPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + super(collectorRegistry); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class ExemplarsConfiguration { + + @Bean + SpanContextSupplier spanContextSupplier() { + return new SpanContextSupplier() { + + @Override + public String getTraceId() { + return null; + } + + @Override + public String getSpanId() { + return null; + } + + @Override + public boolean isSampled() { + return false; + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java index e691fc189744..319ee2393f05 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java @@ -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 @@ * @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 @@ void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.defaults.metrics.export.enabled=false") .run((context) -> assertThat(context) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .doesNotHaveBean(CollectorRegistry.class) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test @@ -88,61 +77,46 @@ void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.prometheus.metrics.export.enabled=false") .run((context) -> assertThat(context) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .doesNotHaveBean(CollectorRegistry.class) - .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .doesNotHaveBean(PrometheusRegistry.class) + .doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test void allowsCustomConfigToBeUsed() { this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class) .hasBean("customConfig")); } @Test void allowsCustomRegistryToBeUsed() { this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .run((context) -> assertThat(context) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class) .hasBean("customRegistry") - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + .hasSingleBean(PrometheusRegistry.class) + .hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)); } @Test void allowsCustomCollectorRegistryToBeUsed() { - this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) - .hasBean("customCollectorRegistry") - .hasSingleBean(CollectorRegistry.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); - } - - @Test - void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { - this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) - .hasSingleBean(ExemplarSampler.class) - .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.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 allowsCustomExemplarSamplerToBeUsed() { + void autoConfiguresPrometheusMeterRegistryIfSpanContextIsPresent() { 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 @@ void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { .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 @@ Clock clock() { static class CustomConfigConfiguration { @Bean - io.micrometer.prometheus.PrometheusConfig customConfig() { + io.micrometer.prometheusmetrics.PrometheusConfig customConfig() { return (key) -> null; } @@ -268,20 +183,21 @@ io.micrometer.prometheus.PrometheusConfig customConfig() { static class CustomRegistryConfiguration { @Bean - io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( - io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { - return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + io.micrometer.prometheusmetrics.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheusmetrics.PrometheusConfig config, PrometheusRegistry prometheusRegistry, + Clock clock) { + return new io.micrometer.prometheusmetrics.PrometheusMeterRegistry(config, prometheusRegistry, clock); } } @Configuration(proxyBeanMethods = false) @Import(BaseConfiguration.class) - static class CustomCollectorRegistryConfiguration { + static class CustomPrometheusRegistryConfiguration { @Bean - CollectorRegistry customCollectorRegistry() { - return new CollectorRegistry(); + PrometheusRegistry customPrometheusRegistry() { + return new PrometheusRegistry(); } } @@ -291,8 +207,8 @@ CollectorRegistry customCollectorRegistry() { static class CustomEndpointConfiguration { @Bean - PrometheusScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { - return new PrometheusScrapeEndpoint(collectorRegistry); + PrometheusScrapeEndpoint customEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); } } @@ -302,24 +218,27 @@ PrometheusScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { 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 f38efdcbc0d7..09f34b55a826 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapterTests.java @@ -29,7 +29,6 @@ * * @author Mirko Sobeck */ -@SuppressWarnings("deprecation") class PrometheusPropertiesConfigAdapterTests extends AbstractPropertiesConfigAdapterTests { @@ -44,14 +43,6 @@ void whenPropertiesDescriptionsIsSetAdapterDescriptionsReturnsIt() { assertThat(new PrometheusPropertiesConfigAdapter(properties).descriptions()).isFalse(); } - @Test - void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() { - PrometheusProperties properties = new PrometheusProperties(); - properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); - assertThat(new PrometheusPropertiesConfigAdapter(properties).histogramFlavor()) - .isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics); - } - @Test void whenPropertiesStepIsSetAdapterStepReturnsIt() { PrometheusProperties properties = new PrometheusProperties(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java index b2297f97358c..cfdd0a4188c8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java @@ -25,15 +25,24 @@ * * @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 000000000000..d409db5f7ba9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientMetricsExportAutoConfigurationTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exemplars.ExemplarSampler; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.DefaultHttpConnectionFactory; +import io.prometheus.client.exporter.HttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager; +import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientMetricsExportAutoConfiguration}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Jonatan Ivanov + */ +@SuppressWarnings({ "removal", "deprecation" }) +@ExtendWith(OutputCaptureExtension.class) +class PrometheusSimpleclientMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withClassLoader(new FilteredClassLoader("io.micrometer.prometheusmetrics.", "io.prometheus.metrics")) + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class)); + + @Test + void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void autoConfiguresItsConfigCollectorRegistryAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.defaults.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.prometheus.metrics.export.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class) + .doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class) + .hasBean("customConfig")); + } + + @Test + void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void allowsCustomCollectorRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class) + .hasBean("customCollectorRegistry") + .hasSingleBean(CollectorRegistry.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)); + } + + @Test + void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .hasSingleBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void allowsCustomExemplarSamplerToBeUsed() { + this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class) + .withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class)) + .run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class) + .getBean(ExemplarSampler.class) + .isSameAs(context.getBean("customExemplarSampler"))); + } + + @Test + void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class) + .doesNotHaveBean(ExemplarSampler.class) + .hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)); + } + + @Test + void addsScrapeEndpointToManagementContext() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .run((context) -> assertThat(context).hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointNotAddedToManagementContextWhenNotExposed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void scrapeEndpointCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=prometheus") + .withPropertyValues("management.endpoint.prometheus.enabled=false") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void allowsCustomScrapeEndpointToBeUsed() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withUserConfiguration(CustomEndpointConfiguration.class) + .run((context) -> assertThat(context).hasBean("customEndpoint") + .hasSingleBean(PrometheusSimpleclientScrapeEndpoint.class)); + } + + @Test + void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); + } + + @Test + void withPushGatewayEnabled(CapturedOutput output) { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> { + assertThat(output).doesNotContain("Invalid PushGateway base url"); + hasGatewayURL(context, "http://localhost:9091/metrics/"); + }); + } + + @Test + void withPushGatewayNoBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(DefaultHttpConnectionFactory.class))); + } + + @Test + void withCustomPushGatewayURL() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080") + .withUserConfiguration(BaseConfiguration.class) + .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); + } + + @Test + void withPushGatewayBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true", + "management.prometheus.metrics.export.pushgateway.username=admin", + "management.prometheus.metrics.export.pushgateway.password=secret") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(BasicAuthHttpConnectionFactory.class))); + } + + private void hasGatewayURL(AssertableApplicationContext context, String url) { + assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + } + + private ContextConsumer hasHttpConnectionFactory( + ThrowingConsumer httpConnectionFactory) { + return (context) -> { + PushGateway pushGateway = getPushGateway(context); + httpConnectionFactory + .accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory")); + }; + } + + private PushGateway getPushGateway(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class); + PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class); + return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway"); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusConfig customConfig() { + return (key) -> null; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry customRegistry( + io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) { + return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomCollectorRegistryConfiguration { + + @Bean + CollectorRegistry customCollectorRegistry() { + return new CollectorRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomEndpointConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class ExemplarsConfiguration { + + @Bean + SpanContextSupplier spanContextSupplier() { + return new SpanContextSupplier() { + + @Override + public String getTraceId() { + return null; + } + + @Override + public String getSpanId() { + return null; + } + + @Override + public boolean isSampled() { + return false; + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusSimpleclientPropertiesConfigAdapterTests.java new file mode 100644 index 000000000000..56edc2f62bc7 --- /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" }) +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 2cc20b2ebb3d..fde7e4d62859 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextSupplierTests.java @@ -23,17 +23,18 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContextSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link LazyTracingSpanContextSupplier}. + * Tests for + * {@link org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier}. * * @author Andy Wilkinson */ +@SuppressWarnings("removal") class LazyTracingSpanContextSupplierTests { private final Tracer tracer = mock(Tracer.class); @@ -62,7 +63,7 @@ public Tracer getIfUnique() throws BeansException { }; - private final LazyTracingSpanContextSupplier spanContextSupplier = new LazyTracingSpanContextSupplier( + private final org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier spanContextSupplier = new org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier( this.objectProvider); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java new file mode 100644 index 000000000000..ffee8531bb70 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/LazyTracingSpanContextTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.tracing.prometheus; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyTracingSpanContext}. + * + * @author Andy Wilkinson + */ +class LazyTracingSpanContextTests { + + private final Tracer tracer = mock(Tracer.class); + + private final ObjectProvider objectProvider = new ObjectProvider<>() { + + @Override + public Tracer getObject() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getObject(Object... args) throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getIfAvailable() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + @Override + public Tracer getIfUnique() throws BeansException { + return LazyTracingSpanContextTests.this.tracer; + } + + }; + + private final LazyTracingSpanContext spanContextSupplier = new LazyTracingSpanContext(this.objectProvider); + + @Test + void whenCurrentSpanIsNullThenSpanIdIsNull() { + assertThat(this.spanContextSupplier.getCurrentSpanId()).isNull(); + } + + @Test + void whenCurrentSpanIsNullThenTraceIdIsNull() { + assertThat(this.spanContextSupplier.getCurrentTraceId()).isNull(); + } + + @Test + void whenCurrentSpanIsNullThenSampledIsFalse() { + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + + @Test + void whenCurrentSpanHasSpanIdThenSpanIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.spanId()).willReturn("span-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentSpanId()).isEqualTo("span-id"); + } + + @Test + void whenCurrentSpanHasTraceIdThenTraceIdIsFromSpan() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.traceId()).willReturn("trace-id"); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentTraceId()).isEqualTo("trace-id"); + } + + @Test + void whenCurrentSpanHasNoSpanIdThenSpanIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentSpanId()).isNull(); + } + + @Test + void whenCurrentSpanHasNoTraceIdThenTraceIdIsNull() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.getCurrentTraceId()).isNull(); + } + + @Test + void whenCurrentSpanIsSampledThenSampledIsTrue() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(true); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isTrue(); + } + + @Test + void whenCurrentSpanIsNotSampledThenSampledIsFalse() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(false); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + + @Test + void whenCurrentSpanHasDeferredSamplingThenSampledIsFalse() { + Span span = mock(Span.class); + given(this.tracer.currentSpan()).willReturn(span); + TraceContext traceContext = mock(TraceContext.class); + given(traceContext.sampled()).willReturn(null); + given(span.context()).willReturn(traceContext); + assertThat(this.spanContextSupplier.isCurrentSpanSampled()).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index bc6030fdf2ac..55a499dde8e3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -22,8 +22,9 @@ 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 @@ 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.prometheus-properties.io.prometheus.exporter.exemplarsOnAllMetricTypes=false") + .run((context) -> { + assertThat(context).hasSingleBean(SpanContext.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(1); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(1); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty(); + }); + } + + @Test void prometheusOpenMetricsOutputShouldContainExemplars() { this.contextRunner.run((context) -> { - 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 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { 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 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { @Configuration(proxyBeanMethods = false) private static final class CustomConfiguration { - static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class); + static final SpanContext SUPPLIER = mock(SpanContext.class); @Bean - SpanContextSupplier customSpanContextSupplier() { + SpanContext customSpanContext() { return SUPPLIER; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000000..b16f687532fa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusSimpleclientExemplarsAutoConfigurationTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.tracing.prometheus; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.common.TextFormat; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrometheusSimpleclientExemplarsAutoConfiguration}. + * + * @author Jonatan Ivanov + */ +@SuppressWarnings("removal") +class PrometheusSimpleclientExemplarsAutoConfigurationTests { + + private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + + private static final Pattern COUNTER_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_count\\{error=\"none\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.sampling.probability=1.0", + "management.metrics.distribution.percentiles-histogram.all=true") + .with(MetricsRun.limitedTo()) + .withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class, + PrometheusSimpleclientExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldNotSupplyBeansIfMicrometerTracingIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldSupplyCustomBeans() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .getBean(SpanContextSupplier.class) + .isSameAs(CustomConfiguration.SUPPLIER)); + } + + @Test + @SuppressWarnings("deprecation") + void prometheusOpenMetricsOutputShouldContainExemplars() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(SpanContextSupplier.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry = context + .getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + Optional counterTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) + .map(COUNTER_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); + }); + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomConfiguration { + + static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class); + + @Bean + SpanContextSupplier customSpanContextSupplier() { + return SUPPLIER; + } + + } + + private record TraceInfo(String traceId, String spanId) { + } + +} diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index c66e297035b6..0c348ca85f40 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -24,7 +24,9 @@ dependencies { optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-jakarta9") optional("io.micrometer:micrometer-tracing") + optional("io.micrometer:micrometer-registry-prometheus") optional("io.micrometer:micrometer-registry-prometheus-simpleclient") + optional("io.prometheus:prometheus-metrics-exposition-formats") optional("io.prometheus:simpleclient_pushgateway") { exclude(group: "javax.xml.bind", module: "jaxb-api") } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java new file mode 100644 index 000000000000..25d18012e787 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusOutputFormat.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.export.prometheus; + +import java.io.IOException; +import java.io.OutputStream; + +import io.prometheus.metrics.expositionformats.ExpositionFormats; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; + +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * A {@link Producible} enum for supported Prometheus formats. + * + * @author Andy Wilkinson + * @since 3.3.0 + */ +public enum PrometheusOutputFormat implements Producible { + + /** + * Prometheus text version 0.0.4. + */ + CONTENT_TYPE_004(PrometheusTextFormatWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getPrometheusTextFormatWriter().write(outputStream, snapshots); + } + + @Override + public boolean isDefault() { + return true; + } + + }, + + /** + * OpenMetrics text version 1.0.0. + */ + CONTENT_TYPE_OPENMETRICS_100(OpenMetricsTextFormatWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getOpenMetricsTextFormatWriter().write(outputStream, snapshots); + } + + }, + + /** + * Prometheus metrics protobuf. + */ + CONTENT_TYPE_PROTOBUF(PrometheusProtobufWriter.CONTENT_TYPE) { + + @Override + void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException { + EXPOSITION_FORMATS.getPrometheusProtobufWriter().write(outputStream, snapshots); + } + + }; + + private static final ExpositionFormats EXPOSITION_FORMATS = ExpositionFormats.init(); + + private final MimeType mimeType; + + PrometheusOutputFormat(String mimeType) { + this.mimeType = MimeTypeUtils.parseMimeType(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + + abstract void write(OutputStream outputStream, MetricSnapshots snapshots) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java index 9f0a1db9bd72..6991f4e1b785 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java @@ -16,14 +16,12 @@ package org.springframework.boot.actuate.metrics.export.prometheus; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Enumeration; import java.util.Set; -import io.prometheus.client.Collector.MetricFamilySamples; -import io.prometheus.client.CollectorRegistry; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -44,30 +42,28 @@ public class PrometheusScrapeEndpoint { private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024; - private final CollectorRegistry collectorRegistry; + private final PrometheusRegistry prometheusRegistry; private volatile int nextMetricsScrapeSize = 16; - public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { - this.collectorRegistry = collectorRegistry; + public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + this.prometheusRegistry = prometheusRegistry; } - @ReadOperation(producesFrom = TextOutputFormat.class) - public WebEndpointResponse scrape(TextOutputFormat format, @Nullable Set includedNames) { + @ReadOperation(producesFrom = PrometheusOutputFormat.class) + public WebEndpointResponse scrape(PrometheusOutputFormat format, @Nullable Set includedNames) { try { - Writer writer = new StringWriter(this.nextMetricsScrapeSize); - Enumeration samples = (includedNames != null) - ? this.collectorRegistry.filteredMetricFamilySamples(includedNames) - : this.collectorRegistry.metricFamilySamples(); - format.write(writer, samples); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize); + MetricSnapshots metricSnapshots = (includedNames != null) + ? this.prometheusRegistry.scrape(includedNames::contains) : this.prometheusRegistry.scrape(); + format.write(outputStream, metricSnapshots); - String scrapePage = writer.toString(); + String scrapePage = outputStream.toString(); this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; return new WebEndpointResponse<>(scrapePage, format); } catch (IOException ex) { - // This actually never happens since StringWriter doesn't throw an IOException throw new IllegalStateException("Writing metrics failed", ex); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java new file mode 100644 index 000000000000..2da3fe0d1e61 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpoint.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.export.prometheus; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Enumeration; +import java.util.Set; + +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.CollectorRegistry; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.lang.Nullable; + +/** + * {@link Endpoint @Endpoint} that uses the Prometheus simpleclient to output metrics in a + * format that can be scraped by the Prometheus server. + * + * @author Jon Schneider + * @author Johnny Lim + * @since 2.0.0 + * @deprecated in favor of {@link PrometheusScrapeEndpoint} + */ +@Deprecated(since = "3.3.0", forRemoval = true) +@WebEndpoint(id = "prometheus") +public class PrometheusSimpleclientScrapeEndpoint { + + private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024; + + private final CollectorRegistry collectorRegistry; + + private volatile int nextMetricsScrapeSize = 16; + + public PrometheusSimpleclientScrapeEndpoint(CollectorRegistry collectorRegistry) { + this.collectorRegistry = collectorRegistry; + } + + @SuppressWarnings("removal") + @ReadOperation(producesFrom = TextOutputFormat.class) + public WebEndpointResponse scrape(TextOutputFormat format, @Nullable Set includedNames) { + try { + Writer writer = new StringWriter(this.nextMetricsScrapeSize); + Enumeration samples = (includedNames != null) + ? this.collectorRegistry.filteredMetricFamilySamples(includedNames) + : this.collectorRegistry.metricFamilySamples(); + format.write(writer, samples); + + String scrapePage = writer.toString(); + this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; + + return new WebEndpointResponse<>(scrapePage, format); + } + catch (IOException ex) { + // This actually never happens since StringWriter doesn't throw an IOException + throw new IllegalStateException("Writing metrics failed", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java index 54b16b7110c9..a49777ed29dd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,9 @@ * * @author Andy Wilkinson * @since 2.5.0 + * @deprecated in favor of {@link PrometheusOutputFormat} */ +@Deprecated(since = "3.3.0", forRemoval = true) public enum TextOutputFormat implements Producible { /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java index 49c3a658db36..6ea796ef3b5a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java @@ -19,8 +19,11 @@ 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 @@ void scrapeHasContentTypeText004ByDefault(WebTestClient client) { @WebEndpointTest void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { - String expectedContentType = TextFormat.CONTENT_TYPE_004; + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; String accept = "*/*;q=0.8"; - assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType); client.get() .uri("/actuator/prometheus") .accept(MediaType.parseMediaType(accept)) @@ -76,7 +77,7 @@ void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter( @WebEndpointTest void scrapeCanProduceOpenMetrics100(WebTestClient client) { - MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); client.get() .uri("/actuator/prometheus") .accept(openMetrics) @@ -93,8 +94,8 @@ void scrapeCanProduceOpenMetrics100(WebTestClient client) { @WebEndpointTest void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { - MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); - MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004); + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + MediaType textPlain = MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE); client.get() .uri("/actuator/prometheus") .accept(openMetrics, textPlain) @@ -108,36 +109,50 @@ void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { @WebEndpointTest void scrapeWithIncludedNames(WebTestClient client) { client.get() - .uri("/actuator/prometheus?includedNames=counter1_total,counter2_total") + .uri("/actuator/prometheus?includedNames=counter1,counter2") .exchange() .expectStatus() .isOk() .expectHeader() - .contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)) + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) .expectBody(String.class) .value((body) -> assertThat(body).contains("counter1_total") .contains("counter2_total") .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 000000000000..94766f193e8c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusSimpleclientScrapeEndpointIntegrationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrometheusSimpleclientScrapeEndpoint}. + * + * @author Jon Schneider + * @author Johnny Lim + */ +@SuppressWarnings("removal") +class PrometheusSimpleclientScrapeEndpointIntegrationTests { + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefault(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + assertThat(TextFormat.chooseContentType(null)).isEqualTo(expectedContentType); + client.get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + String accept = "*/*;q=0.8"; + assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType); + client.get() + .uri("/actuator/prometheus") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeCanProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + } + + @WebEndpointTest + void scrapeWithIncludedNames(WebTestClient client) { + client.get() + .uri("/actuator/prometheus?includedNames=counter1_total,counter2_total") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + PrometheusSimpleclientScrapeEndpoint prometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry); + } + + @Bean + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + @SuppressWarnings("deprecation") + MeterRegistry registry(CollectorRegistry registry) { + io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry( + (k) -> null, registry, Clock.SYSTEM); + Counter.builder("counter1").register(meterRegistry); + Counter.builder("counter2").register(meterRegistry); + Counter.builder("counter3").register(meterRegistry); + return meterRegistry; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java new file mode 100644 index 000000000000..b77c73f6e4df --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/SecondCustomPrometheusScrapeEndpointIntegrationTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.export.prometheus; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; + +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for exposing a {@link PrometheusScrapeEndpoint} and + * {@link PrometheusSimpleclientScrapeEndpoint} with different IDs. + * + * @author Jon Schneider + * @author Johnny Lim + */ +class SecondCustomPrometheusScrapeEndpointIntegrationTests { + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefault(WebTestClient client) { + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; + client.get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + client.get() + .uri("/actuator/prometheussc") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { + String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE; + String accept = "*/*;q=0.8"; + client.get() + .uri("/actuator/prometheus") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + client.get() + .uri("/actuator/prometheussc") + .accept(MediaType.parseMediaType(accept)) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeCanProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + client.get() + .uri("/actuator/prometheussc") + .accept(openMetrics) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE); + MediaType textPlain = MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE); + client.get() + .uri("/actuator/prometheus") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + client.get() + .uri("/actuator/prometheussc") + .accept(openMetrics, textPlain) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(openMetrics); + } + + @WebEndpointTest + void scrapeWithIncludedNames(WebTestClient client) { + client.get() + .uri("/actuator/prometheus?includedNames=counter1,counter2") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + client.get() + .uri("/actuator/prometheussc?includedNames=counter1_total,counter2_total") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE)) + .expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total") + .doesNotContain("counter3_total")); + } + + @SuppressWarnings({ "deprecation", "removal" }) + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) { + return new PrometheusScrapeEndpoint(prometheusRegistry); + } + + @Bean + CustomPrometheusScrapeEndpoint customPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + return new CustomPrometheusScrapeEndpoint(collectorRegistry); + } + + @Bean + PrometheusRegistry prometheusRegistry() { + return new PrometheusRegistry(); + } + + @Bean + CollectorRegistry collectorRegistry() { + return new CollectorRegistry(true); + } + + @Bean + PrometheusMeterRegistry registry(PrometheusRegistry prometheusRegistry) { + return new PrometheusMeterRegistry((k) -> null, prometheusRegistry, Clock.SYSTEM); + } + + @Bean + io.micrometer.prometheus.PrometheusMeterRegistry oldRegistry(CollectorRegistry collectorRegistry) { + return new io.micrometer.prometheus.PrometheusMeterRegistry((k) -> null, collectorRegistry, Clock.SYSTEM); + } + + @Bean + CompositeMeterRegistry compositeMeterRegistry(PrometheusMeterRegistry prometheusMeterRegistry, + io.micrometer.prometheus.PrometheusMeterRegistry prometheusSCMeterRegistry) { + CompositeMeterRegistry composite = new CompositeMeterRegistry(); + composite.add(prometheusMeterRegistry).add(prometheusSCMeterRegistry); + Counter.builder("counter1").register(composite); + Counter.builder("counter2").register(composite); + Counter.builder("counter3").register(composite); + return composite; + } + + @WebEndpoint(id = "prometheussc") + static class CustomPrometheusScrapeEndpoint extends PrometheusSimpleclientScrapeEndpoint { + + CustomPrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { + super(collectorRegistry); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 48142fe4914a..15e5daa15bae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1232,7 +1232,7 @@ bom { ] } } - library("Micrometer", "1.13.0-M2") { + library("Micrometer", "1.13.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ @@ -1415,7 +1415,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"