Merge pull request #40023 from shakuzen

* pr/40023:
  Polish "Auto-config support for latest Prometheus client and simpleclient"
  Auto-config support for latest Prometheus client and simpleclient

Closes gh-40023
This commit is contained in:
Moritz Halbritter
2024-04-05 10:52:32 +02:00
32 changed files with 2346 additions and 326 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) {
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());
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);
PrometheusRegistry prometheusRegistry() {
return new PrometheusRegistry();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint.class)
public static class PrometheusScrapeEndpointConfiguration {
static class PrometheusScrapeEndpointConfiguration {
@SuppressWarnings("removal")
@Bean
@ConditionalOnMissingBean
public PrometheusScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) {
return new PrometheusScrapeEndpoint(collectorRegistry);
}
}
/**
* Configuration for <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 })
PrometheusScrapeEndpoint prometheusEndpoint(PrometheusRegistry prometheusRegistry) {
return new PrometheusScrapeEndpoint(prometheusRegistry);
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for configuring metrics export
@@ -31,7 +32,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Stephane Nicoll
* @since 2.0.0
*/
@SuppressWarnings("deprecation")
@ConfigurationProperties(prefix = "management.prometheus.metrics.export")
public class PrometheusProperties {
@@ -55,7 +55,13 @@ public class PrometheusProperties {
/**
* Histogram type for backing DistributionSummary and Timer.
*/
private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus;
@Deprecated(since = "3.3.0", forRemoval = true)
private HistogramFlavor histogramFlavor = HistogramFlavor.Prometheus;
/**
* Additional properties to pass to the Prometheus client.
*/
private final Map<String, String> properties = new HashMap<>();
/**
* Step size (i.e. reporting frequency) to use.
@@ -70,11 +76,14 @@ public class PrometheusProperties {
this.descriptions = descriptions;
}
public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() {
@Deprecated(since = "3.3.0", forRemoval = true)
@DeprecatedConfigurationProperty(since = "3.3.0",
reason = "No longer supported. Works only when using the Prometheus simpleclient.")
public HistogramFlavor getHistogramFlavor() {
return this.histogramFlavor;
}
public void setHistogramFlavor(io.micrometer.prometheus.HistogramFlavor histogramFlavor) {
public void setHistogramFlavor(HistogramFlavor histogramFlavor) {
this.histogramFlavor = histogramFlavor;
}
@@ -98,6 +107,10 @@ public class PrometheusProperties {
return this.pushgateway;
}
public Map<String, String> getProperties() {
return this.properties;
}
/**
* Configuration options for push-based interaction with Prometheus.
*/
@@ -209,4 +222,16 @@ public class PrometheusProperties {
}
/**
* Prometheus Histogram flavor.
*
* @deprecated since 3.3.0 for removal in 3.5.0
*/
@Deprecated(since = "3.3.0", forRemoval = true)
public enum HistogramFlavor {
Prometheus, VictoriaMetrics;
}
}

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.getProperties();
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 since 3.3.0 for removal in 3.5.0 in favor of
* {@link PrometheusMetricsExportAutoConfiguration}
*/
@Deprecated(since = "3.3.0", forRemoval = true)
@AutoConfiguration(
before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class },
after = { MetricsAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class })
@ConditionalOnBean(Clock.class)
@ConditionalOnClass(PrometheusMeterRegistry.class)
@ConditionalOnEnabledMetricsExport("prometheus")
@EnableConfigurationProperties(PrometheusProperties.class)
public class PrometheusSimpleclientMetricsExportAutoConfiguration {
@Bean
@ConditionalOnMissingBean
PrometheusConfig simpleclientPrometheusConfig(PrometheusProperties prometheusProperties) {
return new PrometheusSimpleclientPropertiesConfigAdapter(prometheusProperties);
}
@Bean
@ConditionalOnMissingBean
io.micrometer.prometheus.PrometheusMeterRegistry simpleclientPrometheusMeterRegistry(
io.micrometer.prometheus.PrometheusConfig prometheusConfig, CollectorRegistry collectorRegistry,
Clock clock, ObjectProvider<ExemplarSampler> exemplarSamplerProvider) {
return new io.micrometer.prometheus.PrometheusMeterRegistry(prometheusConfig, collectorRegistry, clock,
exemplarSamplerProvider.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
CollectorRegistry collectorRegistry() {
return new CollectorRegistry(true);
}
@Bean
@ConditionalOnMissingBean(ExemplarSampler.class)
@ConditionalOnBean(SpanContextSupplier.class)
DefaultExemplarSampler exemplarSampler(SpanContextSupplier spanContextSupplier) {
return new DefaultExemplarSampler(spanContextSupplier);
}
@SuppressWarnings("removal")
@Configuration(proxyBeanMethods = false)
@ConditionalOnAvailableEndpoint(endpoint = PrometheusSimpleclientScrapeEndpoint.class)
static class PrometheusScrapeEndpointConfiguration {
@Bean
@ConditionalOnMissingBean({ PrometheusSimpleclientScrapeEndpoint.class, PrometheusScrapeEndpoint.class })
PrometheusSimpleclientScrapeEndpoint prometheusEndpoint(CollectorRegistry collectorRegistry) {
return new PrometheusSimpleclientScrapeEndpoint(collectorRegistry);
}
}
/**
* Configuration for <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")
static class PrometheusPushGatewayConfiguration {
/**
* The fallback job name. We use 'spring' since there's a history of Prometheus
* spring integration defaulting to that name from when Prometheus integration
* didn't exist in Spring itself.
*/
private static final String FALLBACK_JOB = "spring";
@Bean
@ConditionalOnMissingBean
PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegistry collectorRegistry,
PrometheusProperties prometheusProperties, Environment environment) throws MalformedURLException {
PrometheusProperties.Pushgateway properties = prometheusProperties.getPushgateway();
Duration pushRate = properties.getPushRate();
String job = getJob(properties, environment);
Map<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,71 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus;
import java.time.Duration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter;
/**
* Adapter to convert {@link PrometheusProperties} to a
* {@link io.micrometer.prometheus.PrometheusConfig}.
*
* @author Jon Schneider
* @author Phillip Webb
*/
@SuppressWarnings({ "deprecation", "removal" })
class PrometheusSimpleclientPropertiesConfigAdapter extends PropertiesConfigAdapter<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) {
return switch (properties.getHistogramFlavor()) {
case Prometheus -> io.micrometer.prometheus.HistogramFlavor.Prometheus;
case VictoriaMetrics -> io.micrometer.prometheus.HistogramFlavor.VictoriaMetrics;
};
}
@Override
public Duration step() {
return get(PrometheusProperties::getStep, io.micrometer.prometheus.PrometheusConfig.super::step);
}
}

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,97 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.tracing.prometheus;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.util.function.SingletonSupplier;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Prometheus Exemplars with
* Micrometer Tracing.
*
* @author Jonatan Ivanov
* @since 3.0.0
* @deprecated since 3.3.0 for removal in 3.5.0
*/
@SuppressWarnings("removal")
@Deprecated(forRemoval = true, since = "3.3.0")
@AutoConfiguration(before = PrometheusSimpleclientMetricsExportAutoConfiguration.class,
after = MicrometerTracingAutoConfiguration.class)
@ConditionalOnBean(Tracer.class)
@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class })
public class PrometheusSimpleclientExemplarsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SpanContextSupplier spanContextSupplier(ObjectProvider<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", "removal" })
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.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;
@@ -37,6 +35,7 @@ import org.springframework.lang.Nullable;
*
* @author Jon Schneider
* @author Johnny Lim
* @author Moritz Halbritter
* @since 2.0.0
*/
@WebEndpoint(id = "prometheus")
@@ -44,30 +43,26 @@ public class PrometheusScrapeEndpoint {
private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024;
private final CollectorRegistry collectorRegistry;
private final PrometheusRegistry prometheusRegistry;
private volatile int nextMetricsScrapeSize = 16;
public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) {
this.collectorRegistry = collectorRegistry;
public PrometheusScrapeEndpoint(PrometheusRegistry prometheusRegistry) {
this.prometheusRegistry = prometheusRegistry;
}
@ReadOperation(producesFrom = TextOutputFormat.class)
public WebEndpointResponse<String> scrape(TextOutputFormat format, @Nullable Set<String> includedNames) {
@ReadOperation(producesFrom = PrometheusOutputFormat.class)
public WebEndpointResponse<byte[]> 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);
String scrapePage = writer.toString();
this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA;
return new WebEndpointResponse<>(scrapePage, format);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize);
MetricSnapshots metricSnapshots = (includedNames != null)
? this.prometheusRegistry.scrape(includedNames::contains) : this.prometheusRegistry.scrape();
format.write(outputStream, metricSnapshots);
byte[] content = outputStream.toByteArray();
this.nextMetricsScrapeSize = content.length + METRICS_SCRAPE_CHARS_EXTRA;
return new WebEndpointResponse<>(content, format);
}
catch (IOException ex) {
// This actually never happens since StringWriter doesn't throw an IOException
throw new IllegalStateException("Writing metrics failed", ex);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.export.prometheus;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Set;
import io.prometheus.client.Collector.MetricFamilySamples;
import io.prometheus.client.CollectorRegistry;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
import org.springframework.lang.Nullable;
/**
* {@link Endpoint @Endpoint} that uses the Prometheus simpleclient to output metrics in a
* format that can be scraped by the Prometheus server.
*
* @author Jon Schneider
* @author Johnny Lim
* @since 2.0.0
* @deprecated since 3.3.0 for removal in 3.5.0 in favor of
* {@link PrometheusScrapeEndpoint}
*/
@Deprecated(since = "3.3.0", forRemoval = true)
@WebEndpoint(id = "prometheus")
public class PrometheusSimpleclientScrapeEndpoint {
private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024;
private final CollectorRegistry collectorRegistry;
private volatile int nextMetricsScrapeSize = 16;
public PrometheusSimpleclientScrapeEndpoint(CollectorRegistry collectorRegistry) {
this.collectorRegistry = collectorRegistry;
}
@SuppressWarnings("removal")
@ReadOperation(producesFrom = TextOutputFormat.class)
public WebEndpointResponse<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 since 3.3.0 for removal in 3.5.0 in favor of {@link PrometheusOutputFormat}
*/
@Deprecated(since = "3.3.0", forRemoval = true)
public enum TextOutputFormat implements Producible<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"

View File

@@ -543,11 +543,13 @@ scrape_configs:
----
https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Exemplars] are also supported.
To enable this feature, a `SpanContextSupplier` bean should be present.
To enable this feature, a `SpanContext` bean should be present.
If you're using the deprecated Prometheus simpleclient support and want to enable that feature, a `SpanContextSupplier` bean should be present.
If you use https://micrometer.io/docs/tracing[Micrometer Tracing], this will be auto-configured for you, but you can always create your own if you want.
Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Docs], since this feature needs to be explicitly enabled on Prometheus' side, and it is only supported using the https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#exemplars[OpenMetrics] format.
For ephemeral or batch jobs that may not exist long enough to be scraped, you can use https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support to expose the metrics to Prometheus.
The Prometheus Pushgateway only works with the deprecated Prometheus simpleclient for now, until the Prometheus client 1.x adds support for it.
To enable Prometheus Pushgateway support, add the following dependency to your project:
[source,xml]