From 8f3f0a311868d43bd769547b0624fd06d0ffef0a Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 26 Apr 2023 16:50:38 -0700 Subject: [PATCH] Disable HTTP Observations for Actuator There's something weird with test using WebTestClient, see the two failing tests in WebFluxObservationAutoConfigurationTests: - whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath - whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath Closes gh-34801 --- .../WebFluxObservationAutoConfiguration.java | 22 +++ .../WebMvcObservationAutoConfiguration.java | 48 ++++- ...itional-spring-configuration-metadata.json | 6 + ...FluxObservationAutoConfigurationTests.java | 155 +++++++++++++++- ...bMvcObservationAutoConfigurationTests.java | 175 ++++++++++++++++++ 5 files changed, 403 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 91b1d6fa95aa..51e3f773be47 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -19,6 +19,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; import org.springframework.beans.factory.ObjectProvider; @@ -29,11 +30,13 @@ 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.endpoint.web.PathMappedEndpoints; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -41,6 +44,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.ServerRequestObservationContext; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; import org.springframework.web.filter.reactive.ServerHttpObservationFilter; @@ -51,6 +55,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Jonatan Ivanov * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -97,4 +102,21 @@ MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false") + static class ActuatorWebEndpointObservationConfiguration { + + @Bean + ObservationPredicate actuatorWebEndpointObservationPredicate(PathMappedEndpoints pathMappedEndpoints) { + return (name, context) -> { + if (context instanceof ServerRequestObservationContext serverContext) { + return !serverContext.getCarrier().getURI().getPath().startsWith(pathMappedEndpoints.getBasePath()); + } + return true; + }; + + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java index 2b4aa96c3933..9cf37787f26e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; +import java.nio.file.Path; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.DispatcherType; @@ -30,12 +33,17 @@ 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.endpoint.web.PathMappedEndpoints; 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.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.Servlet; import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -43,6 +51,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationContext; import org.springframework.http.server.observation.ServerRequestObservationConvention; import org.springframework.web.filter.ServerHttpObservationFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -54,6 +63,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Jonatan Ivanov * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -61,7 +71,8 @@ @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass({ DispatcherServlet.class, Observation.class }) @ConditionalOnBean(ObservationRegistry.class) -@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class, ServerProperties.class, + WebMvcProperties.class }) public class WebMvcObservationAutoConfiguration { @Bean @@ -97,4 +108,39 @@ MeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationPrope } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false") + static class ActuatorWebEndpointObservationConfiguration { + + @Bean + ObservationPredicate actuatorWebEndpointObservationPredicate(ServerProperties serverProperties, + WebMvcProperties webMvcProperties, PathMappedEndpoints pathMappedEndpoints) { + return (name, context) -> { + if (context instanceof ServerRequestObservationContext serverContext) { + String endpointPath = getEndpointPath(serverProperties, webMvcProperties, pathMappedEndpoints); + return !serverContext.getCarrier().getRequestURI().startsWith(endpointPath); + } + return true; + }; + } + + private static String getEndpointPath(ServerProperties serverProperties, WebMvcProperties webMvcProperties, + PathMappedEndpoints pathMappedEndpoints) { + String contextPath = getContextPath(serverProperties); + String servletPath = getServletPath(webMvcProperties); + return Path.of(contextPath, servletPath, pathMappedEndpoints.getBasePath()).toString(); + } + + private static String getContextPath(ServerProperties serverProperties) { + Servlet servlet = serverProperties.getServlet(); + return (servlet.getContextPath() != null) ? servlet.getContextPath() : ""; + } + + private static String getServletPath(WebMvcProperties webMvcProperties) { + WebMvcProperties.Servlet servletProperties = webMvcProperties.getServlet(); + return (servletProperties.getPath() != null) ? servletProperties.getPath() : ""; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bd717a474b56..eccf5a80994c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2054,6 +2054,12 @@ "level": "error" } }, + { + "name": "management.observations.http.server.actuator.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable HTTP observations for actuator endpoints.", + "defaultValue": false + }, { "name": "management.otlp.tracing.compression", "defaultValue": "none" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index 48c065660fa0..20bb13b7639e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -19,15 +19,23 @@ import java.util.List; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -52,6 +60,7 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -114,6 +123,121 @@ void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(Captu }); } + @Test + void whenAnActuatorEndpointIsCalledObservationsShouldBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info") + .run((context) -> { + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2) + .hasAnObservationWithAKeyValue("http.url", "/test0") + .hasAnObservationWithAKeyValue("http.url", "/actuator/info"); + }); + } + + @Test + void whenActuatorObservationsEnabledObservationsShouldBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=true") + .run((context) -> { + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2) + .hasAnObservationWithAKeyValue("http.url", "/test0") + .hasAnObservationWithAKeyValue("http.url", "/actuator/info"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomEndpointBasePath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", + "management.endpoints.web.base-path=/management") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/management/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", "spring.webflux.base-path=/test-path") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-path", + context, "/test0", "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", "spring.webflux.base-path=/test-path", + "management.endpoints.web.base-path=/management") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-path", + context, "/test0", "/management/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + @Test void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) @@ -132,8 +256,7 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); } - private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) - throws Exception { + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) { assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); WebTestClient client = WebTestClient.bindToApplicationContext(context).build(); for (String url : urls) { @@ -142,6 +265,34 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati return context.getBean(MeterRegistry.class); } + private TestObservationRegistry getInitializedTestObservationRegistry( + AssertableReactiveWebApplicationContext context, String... urls) { + return getInitializedTestObservationRegistry("", context, urls); + } + + private TestObservationRegistry getInitializedTestObservationRegistry(String baseUrl, + AssertableReactiveWebApplicationContext context, String... urls) { + assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); + WebTestClient client = WebTestClient.bindToApplicationContext(context) + .configureClient() + .baseUrl(baseUrl) + .build(); + for (String url : urls) { + client.get().uri(url).exchange().expectStatus().isOk(); + } + return context.getBean(TestObservationRegistry.class); + } + + @Configuration(proxyBeanMethods = false) + static class TestObservationRegistryConfiguration { + + @Bean + ObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomConventionConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index 3fd1a2b61bef..40c91a629b49 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -19,17 +19,24 @@ import java.util.EnumSet; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -57,6 +64,7 @@ * @author Tadaya Tsuyukubo * @author Madhura Bhave * @author Chanhyeong LEE + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) class WebMvcObservationAutoConfigurationTests { @@ -169,6 +177,146 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } + @Test + void whenAnActuatorEndpointIsCalledObservationsShouldBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info") + .run((context) -> { + assertThat(context).doesNotHaveBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2) + .hasAnObservationWithAKeyValue("http.url", "/test0") + .hasAnObservationWithAKeyValue("http.url", "/actuator/info"); + }); + } + + @Test + void whenActuatorObservationsEnabledObservationsShouldBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=true") + .run((context) -> { + assertThat(context).doesNotHaveBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2) + .hasAnObservationWithAKeyValue("http.url", "/test0") + .hasAnObservationWithAKeyValue("http.url", "/actuator/info"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecorded() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomEndpointBasePath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", + "management.endpoints.web.base-path=/management") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0", + "/management/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomContextPath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", + "server.servlet.context-path=/test-context") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-context", + context, "/test-context/test0", "/test-context/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test-context/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomServletPath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", + "spring.mvc.servlet.path=/test-servlet") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-servlet", + context, "/test-servlet/test0", "/test-servlet/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test-servlet/test0"); + }); + } + + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomContextPathAndCustomServletPathAndCustomEndpointBasePath() { + this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, WebMvcEndpointManagementContextConfiguration.class, + MetricsAutoConfiguration.class, ObservationAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=info", + "management.observations.http.server.actuator.enabled=false", + "server.servlet.context-path=/test-context", "spring.mvc.servlet.path=/test-servlet", + "management.endpoints.web.base-path=/management") + .run((context) -> { + assertThat(context).hasBean("actuatorWebEndpointObservationPredicate"); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry( + "/test-context/test-servlet", context, "/test-context/test-servlet/test0", + "/test-context/test-servlet/management/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test-context/test-servlet/test0"); + }); + } + private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContext context) throws Exception { return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); } @@ -185,6 +333,33 @@ private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContex return context.getBean(MeterRegistry.class); } + private TestObservationRegistry getInitializedTestObservationRegistry(AssertableWebApplicationContext context, + String... urls) throws Exception { + return getInitializedTestObservationRegistry("", context, urls); + } + + private TestObservationRegistry getInitializedTestObservationRegistry(String contextPath, + AssertableWebApplicationContext context, String... urls) throws Exception { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); + assertThat(filter).isInstanceOf(ServerHttpObservationFilter.class); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(filter).build(); + for (String url : urls) { + mockMvc.perform(MockMvcRequestBuilders.get(url).contextPath(contextPath)).andExpect(status().isOk()); + } + return context.getBean(TestObservationRegistry.class); + } + + @Configuration(proxyBeanMethods = false) + static class TestObservationRegistryConfiguration { + + @Bean + ObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + } + @Configuration(proxyBeanMethods = false) static class TestServerHttpObservationFilterRegistrationConfiguration {