Auto-configure Observation instrumentation for WebFlux
Prior to this commit, Spring Boot would offer a specific Metrics instrumentation for WebFlux applications through a `WebFilter` and custom Tag providers. As of Spring Framework 6.0, the Observation instrumentation is done directly in WebFlux, also with a `WebFilter`. While this allows both metrics and traces, some features cannot be supported in the same way with this new infrastructure. The former `WebFilter` has been removed and the Tagging infrastructure deprecated in favor of custom Observation conventions. This commit provides an adapter layer so that developers can refactor their custom tagging solution to the convention way, during the deprecation phase, without losing any feature. Closes gh-32539
This commit is contained in:
parent
cda63b541f
commit
685fa900f8
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* 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.metrics.web.reactive;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server.ServerRequest;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.PropertiesAutoTimer;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
|
||||
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.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
|
||||
* WebFlux applications.
|
||||
*
|
||||
* @author Jon Schneider
|
||||
* @author Dmytro Nosan
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
|
||||
SimpleMetricsExportAutoConfiguration.class })
|
||||
@ConditionalOnBean(MeterRegistry.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
@SuppressWarnings("removal")
|
||||
public class WebFluxMetricsAutoConfiguration {
|
||||
|
||||
private final MetricsProperties properties;
|
||||
|
||||
public WebFluxMetricsAutoConfiguration(MetricsProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(WebFluxTagsProvider.class)
|
||||
public DefaultWebFluxTagsProvider webFluxTagsProvider(ObjectProvider<WebFluxTagsContributor> contributors) {
|
||||
return new DefaultWebFluxTagsProvider(true, contributors.orderedStream().toList());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MetricsWebFilter webfluxMetrics(MeterRegistry registry, WebFluxTagsProvider tagConfigurer) {
|
||||
ServerRequest request = this.properties.getWeb().getServer().getRequest();
|
||||
return new MetricsWebFilter(registry, tagConfigurer, request.getMetricName(), new PropertiesAutoTimer(null));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public MeterFilter metricsHttpServerUriTagFilter() {
|
||||
String metricName = this.properties.getWeb().getServer().getRequest().getMetricName();
|
||||
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
|
||||
() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
|
||||
return MeterFilter.maximumAllowableTags(metricName, "uri", this.properties.getWeb().getServer().getMaxUriTags(),
|
||||
filter);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.client;
|
|||
|
||||
import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.observation.Observation;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
|
||||
import org.springframework.http.client.observation.ClientRequestObservationContext;
|
||||
|
@ -42,11 +41,6 @@ class ClientHttpObservationConventionAdapter implements ClientRequestObservation
|
|||
this.tagsProvider = tagsProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsContext(Observation.Context context) {
|
||||
return context instanceof ClientRequestObservationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.observation.web.reactive;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
|
||||
import org.springframework.http.observation.reactive.ServerRequestObservationContext;
|
||||
import org.springframework.http.observation.reactive.ServerRequestObservationConvention;
|
||||
|
||||
/**
|
||||
* Adapter class that applies {@link WebFluxTagsProvider} tags as a
|
||||
* {@link ServerRequestObservationConvention}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class ServerRequestObservationConventionAdapter implements ServerRequestObservationConvention {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final WebFluxTagsProvider tagsProvider;
|
||||
|
||||
ServerRequestObservationConventionAdapter(String name, WebFluxTagsProvider tagsProvider) {
|
||||
this.name = name;
|
||||
this.tagsProvider = tagsProvider;
|
||||
}
|
||||
|
||||
ServerRequestObservationConventionAdapter(String name, List<WebFluxTagsContributor> contributors) {
|
||||
this.name = name;
|
||||
this.tagsProvider = new DefaultWebFluxTagsProvider(contributors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(context.getServerWebExchange(), context.getError());
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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.observation.web.reactive;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
|
||||
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.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.observation.reactive.DefaultServerRequestObservationConvention;
|
||||
import org.springframework.http.observation.reactive.ServerRequestObservationConvention;
|
||||
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
|
||||
* WebFlux applications.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Jon Schneider
|
||||
* @author Dmytro Nosan
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
|
||||
SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class })
|
||||
@ConditionalOnClass(Observation.class)
|
||||
@ConditionalOnBean(ObservationRegistry.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
|
||||
@SuppressWarnings("removal")
|
||||
public class WebFluxObservationAutoConfiguration {
|
||||
|
||||
private final MetricsProperties metricsProperties;
|
||||
|
||||
private final ObservationProperties observationProperties;
|
||||
|
||||
public WebFluxObservationAutoConfiguration(MetricsProperties metricsProperties,
|
||||
ObservationProperties observationProperties) {
|
||||
this.metricsProperties = metricsProperties;
|
||||
this.observationProperties = observationProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry,
|
||||
ObjectProvider<WebFluxTagsProvider> tagConfigurer,
|
||||
ObjectProvider<WebFluxTagsContributor> contributorsProvider) {
|
||||
|
||||
String observationName = this.observationProperties.getHttp().getServer().getRequests().getName();
|
||||
String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName();
|
||||
String name = (observationName != null) ? observationName : metricName;
|
||||
WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable();
|
||||
List<WebFluxTagsContributor> tagsContributors = contributorsProvider.orderedStream().toList();
|
||||
ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name);
|
||||
if (tagsProvider != null) {
|
||||
convention = new ServerRequestObservationConventionAdapter(name, tagsProvider);
|
||||
}
|
||||
else if (!tagsContributors.isEmpty()) {
|
||||
convention = new ServerRequestObservationConventionAdapter(name, tagsContributors);
|
||||
}
|
||||
return new ServerHttpObservationFilter(registry, convention);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(MeterRegistry.class)
|
||||
@ConditionalOnBean(MeterRegistry.class)
|
||||
static class MeterFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties properties) {
|
||||
String metricName = properties.getWeb().getServer().getRequest().getMetricName();
|
||||
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
|
||||
() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
|
||||
return MeterFilter.maximumAllowableTags(metricName, "uri", properties.getWeb().getServer().getMaxUriTags(),
|
||||
filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* 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.
|
||||
|
@ -15,6 +15,6 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for WebFlux actuator metrics.
|
||||
* Auto-configuration for WebFlux actuator observations.
|
||||
*/
|
||||
package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive;
|
||||
package org.springframework.boot.actuate.autoconfigure.observation.web.reactive;
|
|
@ -79,7 +79,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetric
|
|||
org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.observation.web.reactive.WebFluxObservationAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.data.mongo.MongoHealthContributorAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.data.mongo.MongoReactiveHealthContributorAutoConfiguration
|
||||
|
|
|
@ -41,9 +41,9 @@ import org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetrics
|
|||
import org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.web.reactive.WebFluxObservationAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
|
@ -141,7 +141,7 @@ class MetricsIntegrationTests {
|
|||
SystemMetricsAutoConfiguration.class, RabbitMetricsAutoConfiguration.class,
|
||||
CacheMetricsAutoConfiguration.class, DataSourcePoolMetricsAutoConfiguration.class,
|
||||
HibernateMetricsAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class,
|
||||
WebFluxMetricsAutoConfiguration.class, WebMvcObservationAutoConfiguration.class,
|
||||
WebFluxObservationAutoConfiguration.class, WebMvcObservationAutoConfiguration.class,
|
||||
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
RestTemplateAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class })
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2020 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.web.reactive;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
||||
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
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.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebFluxMetricsAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Dmytro Nosan
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@Disabled("until gh-32539 is fixed")
|
||||
class WebFluxMetricsAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||
.with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(WebFluxMetricsAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldProvideWebFluxMetricsBeans() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).getBeans(MetricsWebFilter.class).hasSize(1);
|
||||
assertThat(context).getBeans(DefaultWebFluxTagsProvider.class).hasSize(1);
|
||||
assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash")
|
||||
.isEqualTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagsProviderWhenIgnoreTrailingSlashIsFalse() {
|
||||
this.contextRunner.withPropertyValues("management.metrics.web.server.request.ignore-trailing-slash=false")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class);
|
||||
assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash")
|
||||
.isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideCustomTagsProvider() {
|
||||
this.contextRunner.withUserConfiguration(CustomWebFluxTagsProviderConfig.class)
|
||||
.run((context) -> assertThat(context).getBeans(WebFluxTagsProvider.class).hasSize(1)
|
||||
.containsKey("customWebFluxTagsProvider"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class))
|
||||
.withUserConfiguration(TestController.class)
|
||||
.withPropertyValues("management.metrics.web.server.max-uri-tags=2").run((context) -> {
|
||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
||||
assertThat(registry.get("http.server.requests").meters()).hasSize(2);
|
||||
assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class))
|
||||
.withUserConfiguration(TestController.class)
|
||||
.withPropertyValues("management.metrics.web.server.max-uri-tags=5").run((context) -> {
|
||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
||||
assertThat(registry.get("http.server.requests").meters()).hasSize(3);
|
||||
assertThat(output)
|
||||
.doesNotContain("Reached the maximum number of URI tags for 'http.server.requests'");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricsAreNotRecordedIfAutoTimeRequestsIsDisabled() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class))
|
||||
.withUserConfiguration(TestController.class)
|
||||
.withPropertyValues("management.metrics.web.server.request.autotime.enabled=false").run((context) -> {
|
||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
||||
assertThat(registry.find("http.server.requests").meter()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenTagContributorsAreDefinedThenTagsProviderUsesThem() {
|
||||
this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class);
|
||||
assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("contributors").asList()
|
||||
.hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) {
|
||||
WebTestClient webTestClient = WebTestClient.bindToApplicationContext(context).build();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
webTestClient.get().uri("/test" + i).exchange().expectStatus().isOk();
|
||||
}
|
||||
return context.getBean(MeterRegistry.class);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomWebFluxTagsProviderConfig {
|
||||
|
||||
@Bean
|
||||
WebFluxTagsProvider customWebFluxTagsProvider() {
|
||||
return mock(WebFluxTagsProvider.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TagsContributorsConfiguration {
|
||||
|
||||
@Bean
|
||||
WebFluxTagsContributor tagContributorOne() {
|
||||
return mock(WebFluxTagsContributor.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
WebFluxTagsContributor tagContributorTwo() {
|
||||
return mock(WebFluxTagsContributor.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.observation.web.reactive;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
|
||||
import org.springframework.http.observation.reactive.ServerRequestObservationContext;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServerRequestObservationConventionAdapter}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class ServerRequestObservationConventionAdapterTests {
|
||||
|
||||
private static final String TEST_METRIC_NAME = "test.metric.name";
|
||||
|
||||
private final MockServerHttpRequest request = MockServerHttpRequest.get("/resource/test").build();
|
||||
|
||||
private final MockServerWebExchange serverWebExchange = MockServerWebExchange.builder(this.request).build();
|
||||
|
||||
private final ServerRequestObservationContext context = new ServerRequestObservationContext(this.serverWebExchange);
|
||||
|
||||
private final ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter(
|
||||
TEST_METRIC_NAME, new DefaultWebFluxTagsProvider());
|
||||
|
||||
@Test
|
||||
void shouldUseConfiguredName() {
|
||||
assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPushTagsAsLowCardinalityKeyValues() {
|
||||
this.serverWebExchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
|
||||
PathPatternParser.defaultInstance.parse("/resource/{name}"));
|
||||
assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
|
||||
KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
|
||||
KeyValue.of("method", "GET"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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.observation.web.reactive;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebFluxObservationAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Dmytro Nosan
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@SuppressWarnings("removal")
|
||||
class WebFluxObservationAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||
.with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
|
||||
WebFluxObservationAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldProvideWebFluxObservationFilter() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ServerHttpObservationFilter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseConventionAdapterWhenCustomTagsProvider() {
|
||||
this.contextRunner.withUserConfiguration(CustomTagsProviderConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
|
||||
assertThat(context).hasSingleBean(WebFluxTagsProvider.class);
|
||||
assertThat(context).getBean(ServerHttpObservationFilter.class).extracting("observationConvention")
|
||||
.isInstanceOf(ServerRequestObservationConventionAdapter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseConventionAdapterWhenCustomTagsContributor() {
|
||||
this.contextRunner.withUserConfiguration(CustomTagsContributorConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
|
||||
assertThat(context).hasSingleBean(WebFluxTagsContributor.class);
|
||||
assertThat(context).getBean(ServerHttpObservationFilter.class).extracting("observationConvention")
|
||||
.isInstanceOf(ServerRequestObservationConventionAdapter.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomTagsProviderConfiguration {
|
||||
|
||||
@Bean
|
||||
WebFluxTagsProvider tagsProvider() {
|
||||
return new DefaultWebFluxTagsProvider();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomTagsContributorConfiguration {
|
||||
|
||||
@Bean
|
||||
WebFluxTagsContributor tagsContributor() {
|
||||
return new CustomTagsContributor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomTagsContributor implements WebFluxTagsContributor {
|
||||
|
||||
@Override
|
||||
public Iterable<Tag> httpRequestTags(ServerWebExchange exchange, Throwable ex) {
|
||||
return Tags.of("custom", "testvalue");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.web.reactive.server;
|
||||
|
||||
/**
|
||||
* Runtime exception that materializes a {@link reactor.core.publisher.SignalType#CANCEL
|
||||
* cancel signal} for the WebFlux server metrics instrumentation.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.5.0
|
||||
* @see MetricsWebFilter
|
||||
*/
|
||||
public class CancelledServerWebExchangeException extends RuntimeException {
|
||||
|
||||
}
|
|
@ -30,7 +30,11 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
* @author Jon Schneider
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
|
||||
* {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention}
|
||||
*/
|
||||
@Deprecated(since = "3.0.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public class DefaultWebFluxTagsProvider implements WebFluxTagsProvider {
|
||||
|
||||
private final boolean ignoreTrailingSlash;
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* 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.metrics.web.reactive.server;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations;
|
||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
/**
|
||||
* Intercepts incoming HTTP requests handled by Spring WebFlux handlers and records
|
||||
* metrics about execution time and results.
|
||||
*
|
||||
* @author Jon Schneider
|
||||
* @author Brian Clozel
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||
public class MetricsWebFilter implements WebFilter {
|
||||
|
||||
private static Log logger = LogFactory.getLog(MetricsWebFilter.class);
|
||||
|
||||
private final MeterRegistry registry;
|
||||
|
||||
private final WebFluxTagsProvider tagsProvider;
|
||||
|
||||
private final String metricName;
|
||||
|
||||
private final AutoTimer autoTimer;
|
||||
|
||||
/**
|
||||
* Create a new {@code MetricsWebFilter}.
|
||||
* @param registry the registry to which metrics are recorded
|
||||
* @param tagsProvider provider for metrics tags
|
||||
* @param metricName name of the metric to record
|
||||
* @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public MetricsWebFilter(MeterRegistry registry, WebFluxTagsProvider tagsProvider, String metricName,
|
||||
AutoTimer autoTimer) {
|
||||
this.registry = registry;
|
||||
this.tagsProvider = tagsProvider;
|
||||
this.metricName = metricName;
|
||||
this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return chain.filter(exchange).transformDeferred((call) -> filter(exchange, call));
|
||||
}
|
||||
|
||||
private Publisher<Void> filter(ServerWebExchange exchange, Mono<Void> call) {
|
||||
long start = System.nanoTime();
|
||||
return call.doOnEach((signal) -> onTerminalSignal(exchange, signal.getThrowable(), start))
|
||||
.doOnCancel(() -> onTerminalSignal(exchange, new CancelledServerWebExchangeException(), start));
|
||||
}
|
||||
|
||||
private void onTerminalSignal(ServerWebExchange exchange, Throwable cause, long start) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
if (response.isCommitted() || cause instanceof CancelledServerWebExchangeException) {
|
||||
record(exchange, cause, start);
|
||||
}
|
||||
else {
|
||||
response.beforeCommit(() -> {
|
||||
record(exchange, cause, start);
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void record(ServerWebExchange exchange, Throwable cause, long start) {
|
||||
try {
|
||||
cause = (cause != null) ? cause : exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
|
||||
Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
|
||||
Set<Timed> annotations = getTimedAnnotations(handler);
|
||||
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
|
||||
long duration = System.nanoTime() - start;
|
||||
AutoTimer.apply(this.autoTimer, this.metricName, annotations,
|
||||
(builder) -> builder.description("Duration of HTTP server request handling").tags(tags)
|
||||
.register(this.registry).record(duration, TimeUnit.NANOSECONDS));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.warn("Failed to record timer metrics", ex);
|
||||
// Allow exchange to continue, unaffected by metrics problem
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Timed> getTimedAnnotations(Object handler) {
|
||||
if (handler instanceof HandlerMethod handlerMethod) {
|
||||
return TimedAnnotations.get(handlerMethod.getMethod(), handlerMethod.getBeanType());
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
}
|
|
@ -40,7 +40,10 @@ import org.springframework.web.util.pattern.PathPattern;
|
|||
* @author Michael McFadyen
|
||||
* @author Brian Clozel
|
||||
* @since 2.0.0
|
||||
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
|
||||
* {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention}
|
||||
*/
|
||||
@Deprecated(since = "3.0.0", forRemoval = true)
|
||||
public final class WebFluxTags {
|
||||
|
||||
private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
|
||||
|
@ -176,8 +179,7 @@ public final class WebFluxTags {
|
|||
*/
|
||||
public static Tag outcome(ServerWebExchange exchange, Throwable exception) {
|
||||
if (exception != null) {
|
||||
if (exception instanceof CancelledServerWebExchangeException
|
||||
|| DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) {
|
||||
if (DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) {
|
||||
return Outcome.UNKNOWN.asTag();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,11 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.3.0
|
||||
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
|
||||
* {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention}
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@Deprecated(since = "3.0.0", forRemoval = true)
|
||||
public interface WebFluxTagsContributor {
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,8 +26,11 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
* @author Jon Schneider
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
|
||||
* {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention}
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@Deprecated(since = "3.0.0", forRemoval = true)
|
||||
public interface WebFluxTagsProvider {
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class DefaultWebFluxTagsProviderTests {
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,283 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.web.reactive.server;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import io.micrometer.core.instrument.MockClock;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.simple.SimpleConfig;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MetricsWebFilter}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class MetricsWebFilterTests {
|
||||
|
||||
private static final String REQUEST_METRICS_NAME = "http.server.requests";
|
||||
|
||||
private static final String REQUEST_METRICS_NAME_PERCENTILE = REQUEST_METRICS_NAME + ".percentile";
|
||||
|
||||
private final FaultyWebFluxTagsProvider tagsProvider = new FaultyWebFluxTagsProvider();
|
||||
|
||||
private SimpleMeterRegistry registry;
|
||||
|
||||
private MetricsWebFilter webFilter;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockClock clock = new MockClock();
|
||||
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock);
|
||||
this.webFilter = new MetricsWebFilter(this.registry, this.tagsProvider, REQUEST_METRICS_NAME,
|
||||
AutoTimer.ENABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistry() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistryForExceptions() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error")))
|
||||
.onErrorResume((t) -> {
|
||||
exchange.getResponse().setRawStatusCode(500);
|
||||
return exchange.getResponse().setComplete();
|
||||
}).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "500");
|
||||
assertMetricsContainsTag("exception", "IllegalStateException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsNonEmptyTagsToRegistryForAnonymousExceptions() {
|
||||
final Exception anonymous = new Exception("test error") {
|
||||
};
|
||||
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(anonymous)).onErrorResume((t) -> {
|
||||
exchange.getResponse().setRawStatusCode(500);
|
||||
return exchange.getResponse().setComplete();
|
||||
}).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "500");
|
||||
assertMetricsContainsTag("exception", anonymous.getClass().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistryForHandledExceptions() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> {
|
||||
exchange.getAttributes().put(ErrorAttributes.ERROR_ATTRIBUTE, new IllegalStateException("test error"));
|
||||
return exchange.getResponse().setComplete();
|
||||
}).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
assertMetricsContainsTag("exception", "IllegalStateException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistryForExceptionsAndCommittedResponse() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> {
|
||||
exchange.getResponse().setRawStatusCode(500);
|
||||
return exchange.getResponse().setComplete().then(Mono.error(new IllegalStateException("test error")));
|
||||
}).onErrorResume((t) -> Mono.empty()).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "500");
|
||||
}
|
||||
|
||||
@Test
|
||||
void trailingSlashShouldNotRecordDuplicateMetrics() {
|
||||
MockServerWebExchange exchange1 = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
MockServerWebExchange exchange2 = createExchange("/projects/spring-boot", "/projects/{project}/");
|
||||
this.webFilter.filter(exchange1, (serverWebExchange) -> exchange1.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
this.webFilter.filter(exchange2, (serverWebExchange) -> exchange2.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("uri", "/projects/{project}").timer().count())
|
||||
.isEqualTo(2);
|
||||
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("status", "200").timer().count()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelledConnectionsShouldProduceMetrics() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
Mono<Void> processing = this.webFilter.filter(exchange,
|
||||
(serverWebExchange) -> exchange.getResponse().setComplete());
|
||||
StepVerifier.create(processing).thenCancel().verify(Duration.ofSeconds(5));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
assertMetricsContainsTag("outcome", "UNKNOWN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void disconnectedExceptionShouldProduceMetrics() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
Mono<Void> processing = this.webFilter
|
||||
.filter(exchange, (serverWebExchange) -> Mono.error(new EOFException("Disconnected")))
|
||||
.onErrorResume((t) -> {
|
||||
exchange.getResponse().setRawStatusCode(500);
|
||||
return exchange.getResponse().setComplete();
|
||||
});
|
||||
StepVerifier.create(processing).expectComplete().verify(Duration.ofSeconds(5));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "500");
|
||||
assertMetricsContainsTag("outcome", "UNKNOWN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsStandardTags() {
|
||||
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timed");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsExtraTags() {
|
||||
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
assertMetricsContainsTag("tag1", "value1");
|
||||
assertMetricsContainsTag("tag2", "value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsExtraTagsAndException() {
|
||||
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error")))
|
||||
.onErrorResume((ex) -> {
|
||||
exchange.getResponse().setRawStatusCode(500);
|
||||
return exchange.getResponse().setComplete();
|
||||
}).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "500");
|
||||
assertMetricsContainsTag("exception", "IllegalStateException");
|
||||
assertMetricsContainsTag("tag1", "value1");
|
||||
assertMetricsContainsTag("tag2", "value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsPercentileMeters() {
|
||||
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedPercentiles");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.95").gauge().value()).isNotZero();
|
||||
assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.5").gauge().value()).isNotZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMetricsRecordingFailsThenExchangeFilteringSucceeds() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.tagsProvider.failOnce();
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
|
||||
.block(Duration.ofSeconds(30));
|
||||
}
|
||||
|
||||
private MockServerWebExchange createTimedHandlerMethodExchange(String methodName) {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE,
|
||||
new HandlerMethod(this, ReflectionUtils.findMethod(Handlers.class, methodName)));
|
||||
return exchange;
|
||||
}
|
||||
|
||||
private MockServerWebExchange createExchange(String path, String pathPattern) {
|
||||
PathPatternParser parser = new PathPatternParser();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build());
|
||||
exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, parser.parse(pathPattern));
|
||||
return exchange;
|
||||
}
|
||||
|
||||
private void assertMetricsContainsTag(String tagKey, String tagValue) {
|
||||
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
static class Handlers {
|
||||
|
||||
@Timed
|
||||
Mono<String> timed() {
|
||||
return Mono.just("test");
|
||||
}
|
||||
|
||||
@Timed(extraTags = { "tag1", "value1", "tag2", "value2" })
|
||||
Mono<String> timedExtraTags() {
|
||||
return Mono.just("test");
|
||||
}
|
||||
|
||||
@Timed(percentiles = { 0.5, 0.95 })
|
||||
Mono<String> timedPercentiles() {
|
||||
return Mono.just("test");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FaultyWebFluxTagsProvider extends DefaultWebFluxTagsProvider {
|
||||
|
||||
private final AtomicBoolean fail = new AtomicBoolean();
|
||||
|
||||
FaultyWebFluxTagsProvider() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Tag> httpRequestTags(ServerWebExchange exchange, Throwable exception) {
|
||||
if (this.fail.compareAndSet(true, false)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return super.httpRequestTags(exchange, exception);
|
||||
}
|
||||
|
||||
void failOnce() {
|
||||
this.fail.set(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -44,6 +44,7 @@ import static org.mockito.Mockito.mock;
|
|||
* @author Madhura Bhave
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class WebFluxTagsTests {
|
||||
|
||||
private MockServerWebExchange exchange;
|
||||
|
@ -215,10 +216,4 @@ class WebFluxTagsTests {
|
|||
assertThat(tag.getValue()).isEqualTo("UNKNOWN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void outcomeTagIsUnknownWhenExceptionIsCancelledExchange() {
|
||||
Tag tag = WebFluxTags.outcome(this.exchange, new CancelledServerWebExchangeException());
|
||||
assertThat(tag.getValue()).isEqualTo("UNKNOWN");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue