Remove ServerHttpObservationFilter from WebFlux

This commit removes the auto-configuration of the
`ServerHttpObservationFilter` bean for WebFlux applications as it's been
deprecated by Spring Framework.

The Observability instrumentation is now handled at the
`WebHttpHandlerBuilder` in Framework directly and doesn't need any
auto-configuration from Spring Boot.

Closes gh-37344
This commit is contained in:
Brian Clozel 2023-09-12 10:59:25 +02:00
parent 8a1f6d4f32
commit c8d036eaa8
3 changed files with 23 additions and 133 deletions

View File

@ -21,9 +21,6 @@ import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.observation.Observation; import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry; 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.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
@ -33,16 +30,10 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention;
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring
@ -53,48 +44,22 @@ import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
* @author Dmytro Nosan * @author Dmytro Nosan
* @since 3.0.0 * @since 3.0.0
*/ */
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @AutoConfiguration(after = { SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class })
SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) @ConditionalOnClass({ Observation.class, MeterRegistry.class })
@ConditionalOnClass(Observation.class) @ConditionalOnBean({ ObservationRegistry.class, MeterRegistry.class })
@ConditionalOnBean(ObservationRegistry.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
@SuppressWarnings("removal")
public class WebFluxObservationAutoConfiguration { public class WebFluxObservationAutoConfiguration {
private final ObservationProperties observationProperties;
public WebFluxObservationAutoConfiguration(ObservationProperties observationProperties) {
this.observationProperties = observationProperties;
}
@Bean @Bean
@ConditionalOnMissingBean(ServerHttpObservationFilter.class) @Order(0)
@Order(Ordered.HIGHEST_PRECEDENCE + 1) MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, ObservationProperties observationProperties) {
ObjectProvider<ServerRequestObservationConvention> customConvention) { String name = observationProperties.getHttp().getServer().getRequests().getName();
String name = this.observationProperties.getHttp().getServer().getRequests().getName(); MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
ServerRequestObservationConvention convention = customConvention () -> "Reached the maximum number of URI tags for '%s'.".formatted(name));
.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
return new ServerHttpObservationFilter(registry, convention); filter);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean(MeterRegistry.class)
static class MeterFilterConfiguration {
@Bean
@Order(0)
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
ObservationProperties observationProperties) {
String name = observationProperties.getHttp().getServer().getRequests().getName();
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
() -> "Reached the maximum number of URI tags for '%s'.".formatted(name));
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
filter);
}
} }
} }

View File

@ -16,12 +16,12 @@
package org.springframework.boot.actuate.autoconfigure.observation.web.reactive; package org.springframework.boot.actuate.autoconfigure.observation.web.reactive;
import java.util.List; import java.time.Duration;
import java.time.temporal.ChronoUnit;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
@ -33,16 +33,6 @@ import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplic
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -54,7 +44,6 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Madhura Bhave * @author Madhura Bhave
*/ */
@ExtendWith(OutputCaptureExtension.class) @ExtendWith(OutputCaptureExtension.class)
@SuppressWarnings("removal")
class WebFluxObservationAutoConfigurationTests { class WebFluxObservationAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
@ -62,31 +51,6 @@ class WebFluxObservationAutoConfigurationTests {
.withConfiguration( .withConfiguration(
AutoConfigurations.of(ObservationAutoConfiguration.class, WebFluxObservationAutoConfiguration.class)); AutoConfigurations.of(ObservationAutoConfiguration.class, WebFluxObservationAutoConfiguration.class));
@Test
void shouldProvideWebFluxObservationFilter() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ServerHttpObservationFilter.class));
}
@Test
void shouldProvideWebFluxObservationFilterOrdered() {
this.contextRunner.withBean(FirstWebFilter.class).withBean(ThirdWebFilter.class).run((context) -> {
List<WebFilter> webFilters = context.getBeanProvider(WebFilter.class).orderedStream().toList();
assertThat(webFilters.get(0)).isInstanceOf(FirstWebFilter.class);
assertThat(webFilters.get(1)).isInstanceOf(ServerHttpObservationFilter.class);
assertThat(webFilters.get(2)).isInstanceOf(ThirdWebFilter.class);
});
}
@Test
void shouldUseCustomConventionWhenAvailable() {
this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
assertThat(context).getBean(ServerHttpObservationFilter.class)
.extracting("observationConvention")
.isInstanceOf(CustomConvention.class);
});
}
@Test @Test
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TestController.class) this.contextRunner.withUserConfiguration(TestController.class)
@ -108,7 +72,7 @@ class WebFluxObservationAutoConfigurationTests {
.withPropertyValues("management.metrics.web.server.max-uri-tags=2", .withPropertyValues("management.metrics.web.server.max-uri-tags=2",
"management.observations.http.server.requests.name=my.http.server.requests") "management.observations.http.server.requests.name=my.http.server.requests")
.run((context) -> { .run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context); MeterRegistry registry = getInitializedMeterRegistry(context, "my.http.server.requests");
assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2); assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'"); assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
}); });
@ -127,53 +91,17 @@ class WebFluxObservationAutoConfigurationTests {
}); });
} }
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) {
throws Exception { return getInitializedMeterRegistry(context, "http.server.requests");
return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2");
} }
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context,
throws Exception { String metricName) {
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
WebTestClient client = WebTestClient.bindToApplicationContext(context).build(); meterRegistry.timer(metricName, "uri", "/test0").record(Duration.of(500, ChronoUnit.SECONDS));
for (String url : urls) { meterRegistry.timer(metricName, "uri", "/test1").record(Duration.of(500, ChronoUnit.SECONDS));
client.get().uri(url).exchange().expectStatus().isOk(); meterRegistry.timer(metricName, "uri", "/test2").record(Duration.of(500, ChronoUnit.SECONDS));
} return meterRegistry;
return context.getBean(MeterRegistry.class);
}
@Configuration(proxyBeanMethods = false)
static class CustomConventionConfiguration {
@Bean
CustomConvention customConvention() {
return new CustomConvention();
}
}
static class CustomConvention extends DefaultServerRequestObservationConvention {
}
@Order(Ordered.HIGHEST_PRECEDENCE)
static class FirstWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
}
}
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
static class ThirdWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
}
} }
} }

View File

@ -245,9 +245,6 @@ When it does so, the orders shown in the following table will be used:
|=== |===
| Web Filter | Order | Web Filter | Order
| `ServerHttpObservationFilter` (Micrometer Observability)
| `Ordered.HIGHEST_PRECEDENCE + 1`
| `WebFilterChainProxy` (Spring Security) | `WebFilterChainProxy` (Spring Security)
| `-100` | `-100`