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:
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user