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:
Brian Clozel 2022-10-20 15:24:47 +02:00
parent cda63b541f
commit 685fa900f8
19 changed files with 378 additions and 704 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
*/
@SuppressWarnings("removal")
class DefaultWebFluxTagsProviderTests {
@Test

View File

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

View File

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