Auto-config support for latest Prometheus client and simpleclient

Deprecates the support for simpleclient but ensures that it can work in
conjunction with support for the latest Prometheus client
auto-configuration.

This involves breaking changes to update public classes to support the
latest Prometheus client. Deprecated support for Prometheus simpleclient
is provided in renamed classes.

See gh-40023
This commit is contained in:
Tommy Ludwig
2024-03-18 23:18:43 +09:00
committed by Moritz Halbritter
parent 2de9969f9a
commit 7f26b67e61
31 changed files with 2334 additions and 320 deletions

View File

@@ -53,6 +53,7 @@ dependencies {
optional("io.micrometer:micrometer-registry-kairos")
optional("io.micrometer:micrometer-registry-new-relic")
optional("io.micrometer:micrometer-registry-otlp")
optional("io.micrometer:micrometer-registry-prometheus")
optional("io.micrometer:micrometer-registry-prometheus-simpleclient")
optional("io.micrometer:micrometer-registry-stackdriver") {
exclude group: "commons-logging", module: "commons-logging"
@@ -141,6 +142,7 @@ dependencies {
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation("io.micrometer:micrometer-observation-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.prometheus:prometheus-metrics-exposition-formats")
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("com.squareup.okhttp3:mockwebserver")
testImplementation("com.jayway.jsonpath:json-path")

View File

@@ -16,18 +16,11 @@
package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Map;
import io.micrometer.core.instrument.Clock;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exemplars.DefaultExemplarSampler;
import io.prometheus.client.exemplars.ExemplarSampler;
import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier;
import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory;
import io.prometheus.client.exporter.PushGateway;
import io.micrometer.prometheusmetrics.PrometheusConfig;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
@@ -35,20 +28,16 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegi
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusSimpleclientScrapeEndpoint;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to Prometheus.
@@ -58,98 +47,43 @@ import org.springframework.util.StringUtils;
* @author Jonatan Ivanov
* @since 2.0.0
*/
@SuppressWarnings("deprecation")
@AutoConfiguration(
before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class },
after = MetricsAutoConfiguration.class)
@ConditionalOnBean(Clock.class)
@ConditionalOnClass(io.micrometer.prometheus.PrometheusMeterRegistry.class)
@ConditionalOnClass(PrometheusMeterRegistry.class)
@ConditionalOnEnabledMetricsExport("prometheus")
@EnableConfigurationProperties(PrometheusProperties.class)
public class PrometheusMetricsExportAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public io.micrometer.prometheus.PrometheusConfig prometheusConfig(PrometheusProperties prometheusProperties) {
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<ExemplarSampler> exemplarSamplerProvider) {
return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock,
exemplarSamplerProvider.getIfAvailable());
public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig,
PrometheusRegistry prometheusRegistry, Clock clock, ObjectProvider<SpanContext> 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 <a href="https://github.com/prometheus/pushgateway">Prometheus
* Pushgateway</a>.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PushGateway.class)
@ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled")
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<String, String> 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);
}
}

View File

@@ -31,7 +31,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Stephane Nicoll
* @since 2.0.0
*/
@SuppressWarnings("deprecation")
@ConfigurationProperties(prefix = "management.prometheus.metrics.export")
public class PrometheusProperties {
@@ -55,7 +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<String, String> prometheusProperties = new HashMap<>();
/**
* Step size (i.e. reporting frequency) to use.
@@ -70,11 +75,11 @@ public class PrometheusProperties {
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 class PrometheusProperties {
return this.pushgateway;
}
public Map<String, String> getPrometheusProperties() {
return this.prometheusProperties;
}
/**
* Configuration options for push-based interaction with Prometheus.
*/
@@ -209,4 +218,13 @@ public class PrometheusProperties {
}
public enum HistogramFlavor {
Prometheus, VictoriaMetrics;
HistogramFlavor() {
}
}
}

View File

@@ -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<PrometheusProperties>
implements io.micrometer.prometheus.PrometheusConfig {
implements PrometheusConfig {
PrometheusPropertiesConfigAdapter(PrometheusProperties properties) {
super(properties);
@@ -47,18 +49,28 @@ class PrometheusPropertiesConfigAdapter extends PropertiesConfigAdapter<Promethe
@Override
public boolean descriptions() {
return get(PrometheusProperties::isDescriptions, io.micrometer.prometheus.PrometheusConfig.super::descriptions);
}
@Override
public io.micrometer.prometheus.HistogramFlavor histogramFlavor() {
return get(PrometheusProperties::getHistogramFlavor,
io.micrometer.prometheus.PrometheusConfig.super::histogramFlavor);
return get(PrometheusProperties::isDescriptions, PrometheusConfig.super::descriptions);
}
@Override
public Duration step() {
return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step);
return get(PrometheusProperties::getStep, PrometheusConfig.super::step);
}
@Override
public Properties prometheusProperties() {
return get(this::fromPropertiesMap, PrometheusConfig.super::prometheusProperties);
}
private Properties fromPropertiesMap(PrometheusProperties prometheusProperties) {
Map<String, String> map = prometheusProperties.getPrometheusProperties();
if (map.isEmpty()) {
return null;
}
Properties properties = PrometheusConfig.super.prometheusProperties();
properties = (properties != null) ? properties : new Properties();
properties.putAll(map);
return properties;
}
}

View File

@@ -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<ExemplarSampler> 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 <a href="https://github.com/prometheus/pushgateway">Prometheus
* Pushgateway</a>.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PushGateway.class)
@ConditionalOnProperty(prefix = "management.prometheus.metrics.export.pushgateway", name = "enabled")
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<String, String> 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;
}
}
}

View File

@@ -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<PrometheusProperties>
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);
}
}

View File

@@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing.prometheus;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration;
@@ -41,42 +41,42 @@ import org.springframework.util.function.SingletonSupplier;
@AutoConfiguration(before = PrometheusMetricsExportAutoConfiguration.class,
after = MicrometerTracingAutoConfiguration.class)
@ConditionalOnBean(Tracer.class)
@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class })
@ConditionalOnClass({ Tracer.class, SpanContext.class })
public class PrometheusExemplarsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SpanContextSupplier spanContextSupplier(ObjectProvider<Tracer> tracerProvider) {
return new LazyTracingSpanContextSupplier(tracerProvider);
SpanContext spanContext(ObjectProvider<Tracer> 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> tracer;
LazyTracingSpanContextSupplier(ObjectProvider<Tracer> tracerProvider) {
LazyTracingSpanContext(ObjectProvider<Tracer> tracerProvider) {
this.tracer = SingletonSupplier.of(tracerProvider::getObject);
}
@Override
public String getTraceId() {
public String getCurrentTraceId() {
Span currentSpan = currentSpan();
return (currentSpan != null) ? currentSpan.context().traceId() : null;
}
@Override
public String getSpanId() {
public String getCurrentSpanId() {
Span currentSpan = currentSpan();
return (currentSpan != null) ? currentSpan.context().spanId() : null;
}
@Override
public boolean isSampled() {
public boolean isCurrentSpanSampled() {
Span currentSpan = currentSpan();
if (currentSpan == null) {
return false;
@@ -85,6 +85,10 @@ public class PrometheusExemplarsAutoConfiguration {
return sampled != null && sampled;
}
@Override
public void markCurrentSpanAsExemplar() {
}
private Span currentSpan() {
return this.tracer.obtain().currentSpan();
}

View File

@@ -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<Tracer> 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> tracer;
LazyTracingSpanContextSupplier(ObjectProvider<Tracer> 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();
}
}
}

View File

@@ -64,6 +64,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.kairos.KairosMetri
org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic.NewRelicMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.signalfx.SignalFxMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration
@@ -109,6 +110,7 @@ org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfigurati
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration
org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontAutoConfiguration

View File

@@ -18,8 +18,9 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentatio
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.prometheus.client.CollectorRegistry;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.client.exporter.common.TextFormat;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
@@ -72,13 +73,12 @@ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocument
static class TestConfiguration {
@Bean
@SuppressWarnings("deprecation")
PrometheusScrapeEndpoint endpoint() {
CollectorRegistry collectorRegistry = new CollectorRegistry(true);
io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry(
(key) -> null, collectorRegistry, Clock.SYSTEM);
PrometheusRegistry prometheusRegistry = new PrometheusRegistry();
PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((key) -> null, prometheusRegistry,
Clock.SYSTEM);
new JvmMemoryMetrics().bindTo(meterRegistry);
return new PrometheusScrapeEndpoint(collectorRegistry);
return new PrometheusScrapeEndpoint(prometheusRegistry);
}
}

View File

@@ -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);
}
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics;
import io.micrometer.atlas.AtlasMeterRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.export.atlas.AtlasMetricsExportAutoConfiguration;
@@ -36,7 +37,6 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Jon Schneider
* @author Andy Wilkinson
*/
@SuppressWarnings("deprecation")
class MeterRegistryCustomizerTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -63,7 +63,7 @@ class MeterRegistryCustomizerTests {
@Test
void customizersCanBeAppliedToSpecificRegistryTypes() {
this.contextRunner.withUserConfiguration(MeterRegistryCustomizerConfiguration.class).run((context) -> {
MeterRegistry prometheus = context.getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class);
MeterRegistry prometheus = context.getBean(PrometheusMeterRegistry.class);
prometheus.get("jvm.memory.used").tags("job", "myjob").gauge();
MeterRegistry atlas = context.getBean(AtlasMeterRegistry.class);
assertThat(atlas.find("jvm.memory.used").tags("job", "myjob").gauge()).isNull();
@@ -79,7 +79,7 @@ class MeterRegistryCustomizerTests {
}
@Bean
MeterRegistryCustomizer<io.micrometer.prometheus.PrometheusMeterRegistry> prometheusOnlyCommonTags() {
MeterRegistryCustomizer<PrometheusMeterRegistry> prometheusOnlyCommonTags() {
return (registry) -> registry.config().commonTags("job", "myjob");
}

View File

@@ -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<AssertableApplicationContext> hasHttpConnectionFactory(
ThrowingConsumer<HttpConnectionFactory> httpConnectionFactory) {
return (context) -> {
PushGateway pushGateway = getPushGateway(context);
httpConnectionFactory
.accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory"));
};
}
private PushGateway getPushGateway(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class);
PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class);
return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway");
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@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;
}
};
}
}
}

View File

@@ -17,33 +17,22 @@
package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus;
import io.micrometer.core.instrument.Clock;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exemplars.ExemplarSampler;
import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier;
import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory;
import io.prometheus.client.exporter.DefaultHttpConnectionFactory;
import io.prometheus.client.exporter.HttpConnectionFactory;
import io.prometheus.client.exporter.PushGateway;
import org.assertj.core.api.ThrowingConsumer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PrometheusMetricsExportAutoConfiguration}.
@@ -52,25 +41,25 @@ import static org.mockito.Mockito.mock;
* @author Stephane Nicoll
* @author Jonatan Ivanov
*/
@SuppressWarnings("deprecation")
@ExtendWith(OutputCaptureExtension.class)
class PrometheusMetricsExportAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withClassLoader(new FilteredClassLoader("io.micrometer.prometheus.", "io.prometheus.client"))
.withConfiguration(AutoConfigurations.of(PrometheusMetricsExportAutoConfiguration.class));
@Test
void backsOffWithoutAClock() {
this.contextRunner.run((context) -> assertThat(context)
.doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class));
.doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class));
}
@Test
void autoConfiguresItsConfigCollectorRegistryAndMeterRegistry() {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.hasSingleBean(CollectorRegistry.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class));
.run((context) -> assertThat(context)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.hasSingleBean(PrometheusRegistry.class)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class));
}
@Test
@@ -78,9 +67,9 @@ class PrometheusMetricsExportAutoConfigurationTests {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.withPropertyValues("management.defaults.metrics.export.enabled=false")
.run((context) -> assertThat(context)
.doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.doesNotHaveBean(CollectorRegistry.class)
.doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class));
.doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.doesNotHaveBean(PrometheusRegistry.class)
.doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class));
}
@Test
@@ -88,61 +77,46 @@ class PrometheusMetricsExportAutoConfigurationTests {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.withPropertyValues("management.prometheus.metrics.export.enabled=false")
.run((context) -> assertThat(context)
.doesNotHaveBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.doesNotHaveBean(CollectorRegistry.class)
.doesNotHaveBean(io.micrometer.prometheus.PrometheusConfig.class));
.doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.doesNotHaveBean(PrometheusRegistry.class)
.doesNotHaveBean(io.micrometer.prometheusmetrics.PrometheusConfig.class));
}
@Test
void allowsCustomConfigToBeUsed() {
this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.hasSingleBean(CollectorRegistry.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class)
.run((context) -> assertThat(context)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.hasSingleBean(PrometheusRegistry.class)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class)
.hasBean("customConfig"));
}
@Test
void allowsCustomRegistryToBeUsed() {
this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.run((context) -> assertThat(context)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.hasBean("customRegistry")
.hasSingleBean(CollectorRegistry.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class));
.hasSingleBean(PrometheusRegistry.class)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class));
}
@Test
void allowsCustomCollectorRegistryToBeUsed() {
this.contextRunner.withUserConfiguration(CustomCollectorRegistryConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class)
.hasBean("customCollectorRegistry")
.hasSingleBean(CollectorRegistry.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusConfig.class));
this.contextRunner.withUserConfiguration(CustomPrometheusRegistryConfiguration.class)
.run((context) -> assertThat(context)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusMeterRegistry.class)
.hasBean("customPrometheusRegistry")
.hasSingleBean(PrometheusRegistry.class)
.hasSingleBean(io.micrometer.prometheusmetrics.PrometheusConfig.class));
}
@Test
void autoConfiguresExemplarSamplerIfSpanContextSupplierIsPresent() {
void autoConfiguresPrometheusMeterRegistryIfSpanContextIsPresent() {
this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class)
.hasSingleBean(ExemplarSampler.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class));
}
@Test
void allowsCustomExemplarSamplerToBeUsed() {
this.contextRunner.withUserConfiguration(ExemplarsConfiguration.class)
.withBean("customExemplarSampler", ExemplarSampler.class, () -> mock(ExemplarSampler.class))
.run((context) -> assertThat(context).hasSingleBean(ExemplarSampler.class)
.getBean(ExemplarSampler.class)
.isSameAs(context.getBean("customExemplarSampler")));
}
@Test
void exemplarSamplerIsNotAutoConfiguredIfSpanContextSupplierIsMissing() {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)
.doesNotHaveBean(ExemplarSampler.class)
.hasSingleBean(io.micrometer.prometheus.PrometheusMeterRegistry.class));
.run((context) -> assertThat(context).hasSingleBean(SpanContext.class)
.hasSingleBean(PrometheusMeterRegistry.class));
}
@Test
@@ -183,65 +157,6 @@ class PrometheusMetricsExportAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class));
}
@Test
void withPushGatewayEnabled(CapturedOutput output) {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> {
assertThat(output).doesNotContain("Invalid PushGateway base url");
hasGatewayURL(context, "http://localhost:9091/metrics/");
});
}
@Test
void withPushGatewayNoBasicAuth() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true")
.withUserConfiguration(BaseConfiguration.class)
.run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory)
.isInstanceOf(DefaultHttpConnectionFactory.class)));
}
@Test
void withCustomPushGatewayURL() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.base-url=https://example.com:8080")
.withUserConfiguration(BaseConfiguration.class)
.run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/"));
}
@Test
void withPushGatewayBasicAuth() {
this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class))
.withPropertyValues("management.prometheus.metrics.export.pushgateway.enabled=true",
"management.prometheus.metrics.export.pushgateway.username=admin",
"management.prometheus.metrics.export.pushgateway.password=secret")
.withUserConfiguration(BaseConfiguration.class)
.run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory)
.isInstanceOf(BasicAuthHttpConnectionFactory.class)));
}
private void hasGatewayURL(AssertableApplicationContext context, String url) {
assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url);
}
private ContextConsumer<AssertableApplicationContext> hasHttpConnectionFactory(
ThrowingConsumer<HttpConnectionFactory> httpConnectionFactory) {
return (context) -> {
PushGateway pushGateway = getPushGateway(context);
httpConnectionFactory
.accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory"));
};
}
private PushGateway getPushGateway(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class);
PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class);
return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway");
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@@ -257,7 +172,7 @@ class PrometheusMetricsExportAutoConfigurationTests {
static class CustomConfigConfiguration {
@Bean
io.micrometer.prometheus.PrometheusConfig customConfig() {
io.micrometer.prometheusmetrics.PrometheusConfig customConfig() {
return (key) -> null;
}
@@ -268,20 +183,21 @@ class PrometheusMetricsExportAutoConfigurationTests {
static class CustomRegistryConfiguration {
@Bean
io.micrometer.prometheus.PrometheusMeterRegistry customRegistry(
io.micrometer.prometheus.PrometheusConfig config, CollectorRegistry collectorRegistry, Clock clock) {
return new io.micrometer.prometheus.PrometheusMeterRegistry(config, collectorRegistry, clock);
io.micrometer.prometheusmetrics.PrometheusMeterRegistry customRegistry(
io.micrometer.prometheusmetrics.PrometheusConfig config, PrometheusRegistry prometheusRegistry,
Clock clock) {
return new io.micrometer.prometheusmetrics.PrometheusMeterRegistry(config, prometheusRegistry, clock);
}
}
@Configuration(proxyBeanMethods = false)
@Import(BaseConfiguration.class)
static class CustomCollectorRegistryConfiguration {
static class CustomPrometheusRegistryConfiguration {
@Bean
CollectorRegistry customCollectorRegistry() {
return new CollectorRegistry();
PrometheusRegistry customPrometheusRegistry() {
return new PrometheusRegistry();
}
}
@@ -291,8 +207,8 @@ class PrometheusMetricsExportAutoConfigurationTests {
static class CustomEndpointConfiguration {
@Bean
PrometheusScrapeEndpoint customEndpoint(CollectorRegistry collectorRegistry) {
return new PrometheusScrapeEndpoint(collectorRegistry);
PrometheusScrapeEndpoint customEndpoint(PrometheusRegistry prometheusRegistry) {
return new PrometheusScrapeEndpoint(prometheusRegistry);
}
}
@@ -302,24 +218,27 @@ class PrometheusMetricsExportAutoConfigurationTests {
static class ExemplarsConfiguration {
@Bean
SpanContextSupplier spanContextSupplier() {
return new SpanContextSupplier() {
SpanContext spanContext() {
return new SpanContext() {
@Override
public String getTraceId() {
public String getCurrentTraceId() {
return null;
}
@Override
public String getSpanId() {
public String getCurrentSpanId() {
return null;
}
@Override
public boolean isSampled() {
public boolean isCurrentSpanSampled() {
return false;
}
@Override
public void markCurrentSpanAsExemplar() {
}
};
}

View File

@@ -29,7 +29,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Mirko Sobeck
*/
@SuppressWarnings("deprecation")
class PrometheusPropertiesConfigAdapterTests
extends AbstractPropertiesConfigAdapterTests<PrometheusProperties, PrometheusPropertiesConfigAdapter> {
@@ -44,14 +43,6 @@ class PrometheusPropertiesConfigAdapterTests
assertThat(new PrometheusPropertiesConfigAdapter(properties).descriptions()).isFalse();
}
@Test
void whenPropertiesHistogramFlavorIsSetAdapterHistogramFlavorReturnsIt() {
PrometheusProperties properties = new PrometheusProperties();
properties.setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics);
assertThat(new PrometheusPropertiesConfigAdapter(properties).histogramFlavor())
.isEqualTo(io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics);
}
@Test
void whenPropertiesStepIsSetAdapterStepReturnsIt() {
PrometheusProperties properties = new PrometheusProperties();

View File

@@ -25,15 +25,24 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Stephane Nicoll
*/
@SuppressWarnings("deprecation")
class PrometheusPropertiesTests {
@Test
void defaultValuesAreConsistent() {
PrometheusProperties properties = new PrometheusProperties();
io.micrometer.prometheusmetrics.PrometheusConfig config = io.micrometer.prometheusmetrics.PrometheusConfig.DEFAULT;
assertThat(properties.isDescriptions()).isEqualTo(config.descriptions());
assertThat(properties.getStep()).isEqualTo(config.step());
}
@SuppressWarnings("deprecation")
@Test
void defaultValuesAreConsistentWithSimpleclient() {
PrometheusProperties properties = new PrometheusProperties();
io.micrometer.prometheus.PrometheusConfig config = io.micrometer.prometheus.PrometheusConfig.DEFAULT;
assertThat(properties.isDescriptions()).isEqualTo(config.descriptions());
assertThat(properties.getHistogramFlavor()).isEqualTo(config.histogramFlavor());
assertThat(PrometheusSimpleclientPropertiesConfigAdapter.mapToMicrometerHistogramFlavor(properties))
.isEqualTo(config.histogramFlavor());
assertThat(properties.getStep()).isEqualTo(config.step());
}

View File

@@ -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<AssertableApplicationContext> hasHttpConnectionFactory(
ThrowingConsumer<HttpConnectionFactory> httpConnectionFactory) {
return (context) -> {
PushGateway pushGateway = getPushGateway(context);
httpConnectionFactory
.accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory"));
};
}
private PushGateway getPushGateway(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class);
PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class);
return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway");
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@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;
}
};
}
}
}

View File

@@ -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<PrometheusProperties, PrometheusSimpleclientPropertiesConfigAdapter> {
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));
}
}

View File

@@ -23,17 +23,18 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContextSupplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LazyTracingSpanContextSupplier}.
* Tests for
* {@link org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier}.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("removal")
class LazyTracingSpanContextSupplierTests {
private final Tracer tracer = mock(Tracer.class);
@@ -62,7 +63,7 @@ class LazyTracingSpanContextSupplierTests {
};
private final LazyTracingSpanContextSupplier spanContextSupplier = new LazyTracingSpanContextSupplier(
private final org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier spanContextSupplier = new org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusSimpleclientExemplarsAutoConfiguration.LazyTracingSpanContextSupplier(
this.objectProvider);
@Test

View File

@@ -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<Tracer> 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();
}
}

View File

@@ -22,8 +22,9 @@ import java.util.regex.Pattern;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier;
import io.prometheus.client.exporter.common.TextFormat;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration;
@@ -49,10 +50,10 @@ import static org.mockito.Mockito.mock;
class PrometheusExemplarsAutoConfigurationTests {
private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile(
"^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
"^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
private static final Pattern COUNTER_TRACE_INFO_PATTERN = Pattern.compile(
"^test_observation_seconds_count\\{error=\"none\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
private static final Pattern COUNT_TRACE_INFO_PATTERN = Pattern.compile(
"^test_observation_seconds_count\\{error=\"none\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.tracing.sampling.probability=1.0",
@@ -64,34 +65,59 @@ class PrometheusExemplarsAutoConfigurationTests {
@Test
void shouldNotSupplyBeansIfPrometheusSupportIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class));
this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.metrics.tracer"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class));
}
@Test
void shouldNotSupplyBeansIfMicrometerTracingIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class));
.run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class));
}
@Test
void shouldSupplyCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class)
.getBean(SpanContextSupplier.class)
.run((context) -> assertThat(context).hasSingleBean(SpanContext.class)
.getBean(SpanContext.class)
.isSameAs(CustomConfiguration.SUPPLIER));
}
@Test
@SuppressWarnings("deprecation")
void prometheusOpenMetricsOutputWithoutExemplarsOnHistogramCount() {
this.contextRunner.withPropertyValues(
"management.prometheus.metrics.export.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<TraceInfo> bucketTraceInfo = openMetricsOutput.lines()
.filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id"))
.map(BUCKET_TRACE_INFO_PATTERN::matcher)
.flatMap(Matcher::results)
.map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1)))
.findFirst();
assertThat(bucketTraceInfo).isNotEmpty();
});
}
@Test
void prometheusOpenMetricsOutputShouldContainExemplars() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(SpanContextSupplier.class);
assertThat(context).hasSingleBean(SpanContext.class);
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation.start("test.observation", observationRegistry).stop();
io.micrometer.prometheus.PrometheusMeterRegistry prometheusMeterRegistry = context
.getBean(io.micrometer.prometheus.PrometheusMeterRegistry.class);
String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class);
String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE);
assertThat(openMetricsOutput).contains("test_observation_seconds_bucket");
assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count");
@@ -107,7 +133,7 @@ class PrometheusExemplarsAutoConfigurationTests {
Optional<TraceInfo> counterTraceInfo = openMetricsOutput.lines()
.filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id"))
.map(COUNTER_TRACE_INFO_PATTERN::matcher)
.map(COUNT_TRACE_INFO_PATTERN::matcher)
.flatMap(Matcher::results)
.map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1)))
.findFirst();
@@ -119,10 +145,10 @@ class PrometheusExemplarsAutoConfigurationTests {
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class);
static final SpanContext SUPPLIER = mock(SpanContext.class);
@Bean
SpanContextSupplier customSpanContextSupplier() {
SpanContext customSpanContext() {
return SUPPLIER;
}

View File

@@ -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<TraceInfo> 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<TraceInfo> 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) {
}
}

View File

@@ -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")
}

View File

@@ -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<PrometheusOutputFormat> {
/**
* 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;
}

View File

@@ -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<String> scrape(TextOutputFormat format, @Nullable Set<String> includedNames) {
@ReadOperation(producesFrom = PrometheusOutputFormat.class)
public WebEndpointResponse<String> scrape(PrometheusOutputFormat format, @Nullable Set<String> includedNames) {
try {
Writer writer = new StringWriter(this.nextMetricsScrapeSize);
Enumeration<MetricFamilySamples> 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);
}
}

View File

@@ -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<String> scrape(TextOutputFormat format, @Nullable Set<String> includedNames) {
try {
Writer writer = new StringWriter(this.nextMetricsScrapeSize);
Enumeration<MetricFamilySamples> 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);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,7 +32,9 @@ import org.springframework.util.MimeTypeUtils;
*
* @author Andy Wilkinson
* @since 2.5.0
* @deprecated in favor of {@link PrometheusOutputFormat}
*/
@Deprecated(since = "3.3.0", forRemoval = true)
public enum TextOutputFormat implements Producible<TextOutputFormat> {
/**

View File

@@ -19,8 +19,11 @@ package org.springframework.boot.actuate.metrics.export.prometheus;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter;
import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.context.annotation.Bean;
@@ -40,8 +43,7 @@ class PrometheusScrapeEndpointIntegrationTests {
@WebEndpointTest
void scrapeHasContentTypeText004ByDefault(WebTestClient client) {
String expectedContentType = TextFormat.CONTENT_TYPE_004;
assertThat(TextFormat.chooseContentType(null)).isEqualTo(expectedContentType);
String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE;
client.get()
.uri("/actuator/prometheus")
.exchange()
@@ -57,9 +59,8 @@ class PrometheusScrapeEndpointIntegrationTests {
@WebEndpointTest
void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) {
String expectedContentType = TextFormat.CONTENT_TYPE_004;
String expectedContentType = PrometheusTextFormatWriter.CONTENT_TYPE;
String accept = "*/*;q=0.8";
assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType);
client.get()
.uri("/actuator/prometheus")
.accept(MediaType.parseMediaType(accept))
@@ -76,7 +77,7 @@ class PrometheusScrapeEndpointIntegrationTests {
@WebEndpointTest
void scrapeCanProduceOpenMetrics100(WebTestClient client) {
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE);
client.get()
.uri("/actuator/prometheus")
.accept(openMetrics)
@@ -93,8 +94,8 @@ class PrometheusScrapeEndpointIntegrationTests {
@WebEndpointTest
void scrapePrefersToProduceOpenMetrics100(WebTestClient client) {
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004);
MediaType openMetrics = MediaType.parseMediaType(OpenMetricsTextFormatWriter.CONTENT_TYPE);
MediaType textPlain = MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE);
client.get()
.uri("/actuator/prometheus")
.accept(openMetrics, textPlain)
@@ -108,36 +109,50 @@ class PrometheusScrapeEndpointIntegrationTests {
@WebEndpointTest
void scrapeWithIncludedNames(WebTestClient client) {
client.get()
.uri("/actuator/prometheus?includedNames=counter1_total,counter2_total")
.uri("/actuator/prometheus?includedNames=counter1,counter2")
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004))
.contentType(MediaType.parseMediaType(PrometheusTextFormatWriter.CONTENT_TYPE))
.expectBody(String.class)
.value((body) -> assertThat(body).contains("counter1_total")
.contains("counter2_total")
.doesNotContain("counter3_total"));
}
@WebEndpointTest
void scrapeCanProducePrometheusProtobuf(WebTestClient client) {
MediaType prometheusProtobuf = MediaType.parseMediaType(PrometheusProtobufWriter.CONTENT_TYPE);
client.get()
.uri("/actuator/prometheus")
.accept(prometheusProtobuf)
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(prometheusProtobuf)
.expectBody(byte[].class)
.value((body) -> assertThat(body).isNotEmpty());
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
PrometheusScrapeEndpoint prometheusScrapeEndpoint(CollectorRegistry collectorRegistry) {
return new PrometheusScrapeEndpoint(collectorRegistry);
PrometheusScrapeEndpoint prometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) {
return new PrometheusScrapeEndpoint(prometheusRegistry);
}
@Bean
CollectorRegistry collectorRegistry() {
return new CollectorRegistry(true);
PrometheusRegistry prometheusRegistry() {
return new PrometheusRegistry();
}
@Bean
@SuppressWarnings("deprecation")
MeterRegistry registry(CollectorRegistry registry) {
io.micrometer.prometheus.PrometheusMeterRegistry meterRegistry = new io.micrometer.prometheus.PrometheusMeterRegistry(
(k) -> null, registry, Clock.SYSTEM);
MeterRegistry registry(PrometheusRegistry prometheusRegistry) {
PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((k) -> null, prometheusRegistry,
Clock.SYSTEM);
Counter.builder("counter1").register(meterRegistry);
Counter.builder("counter2").register(meterRegistry);
Counter.builder("counter3").register(meterRegistry);

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -1241,7 +1241,7 @@ bom {
]
}
}
library("Micrometer", "1.13.0-M2") {
library("Micrometer", "1.13.0-SNAPSHOT") {
considerSnapshots()
group("io.micrometer") {
modules = [
@@ -1424,7 +1424,18 @@ bom {
releaseNotes("https://github.com/pgjdbc/pgjdbc/releases/tag/REL{version}")
}
}
library("Prometheus Client", "0.16.0") {
library("Prometheus Client", "1.2.1") {
group("io.prometheus") {
imports = [
"prometheus-metrics-bom"
]
}
links {
site("https://github.com/prometheus/client_java")
releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}")
}
}
library("Prometheus Simpleclient", "0.16.0") {
group("io.prometheus") {
imports = [
"simpleclient_bom"