From c2e5352970effc697c324021b0a2345080fde297 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 --- .../observation/ObservationProperties.java | 24 +++ .../WebFluxObservationAutoConfiguration.java | 25 ++- .../WebMvcObservationAutoConfiguration.java | 43 ++++- ...FluxObservationAutoConfigurationTests.java | 155 +++++++++++++++- ...bMvcObservationAutoConfigurationTests.java | 175 ++++++++++++++++++ .../build.gradle | 2 +- .../src/main/resources/application.properties | 8 + .../build.gradle | 1 + .../src/main/resources/application.properties | 8 + 9 files changed, 436 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 24d81daa4b1a..513f9da216d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -28,6 +28,7 @@ * * @author Brian Clozel * @author Moritz Halbritter + * @author Jonatan Ivanov * @since 3.0.0 */ @ConfigurationProperties("management.observations") @@ -113,6 +114,8 @@ public static class Server { private final Filter filter = new Filter(); + private final Actuator actuator = new Actuator(); + public ServerRequests getRequests() { return this.requests; } @@ -121,6 +124,10 @@ public Filter getFilter() { return this.filter; } + public Actuator getActuator() { + return this.actuator; + } + public static class ServerRequests { /** @@ -159,4 +166,21 @@ public void setOrder(int order) { } + public static class Actuator { + + /** + * Enables HTTP observations for actuator. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } 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 457d2cdf3937..a2ecec6eb0b1 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,9 +19,11 @@ 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; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; @@ -29,17 +31,20 @@ 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; import org.springframework.context.annotation.Configuration; 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 +56,7 @@ * @author Jon Schneider * @author Dmytro Nosan * @author Moritz Halbritter + * @author Jonatan Ivanov * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -58,7 +64,7 @@ @ConditionalOnClass(Observation.class) @ConditionalOnBean(ObservationRegistry.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) -@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class, WebEndpointProperties.class }) @SuppressWarnings("removal") public class WebFluxObservationAutoConfiguration { @@ -97,4 +103,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 e6f1c487def1..bf3d142fb0a3 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 @@ -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 jakarta.servlet.DispatcherType; @@ -30,18 +31,24 @@ 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; import org.springframework.context.annotation.Configuration; 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 +61,7 @@ * @author Jon Schneider * @author Dmytro Nosan * @author Moritz Halbritter + * @author Jonatan Ivanov * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -61,7 +69,7 @@ @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 }) public class WebMvcObservationAutoConfiguration { @Bean @@ -97,4 +105,37 @@ 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 contextPath = getContextPath(serverProperties); + String servletPath = getServletPath(webMvcProperties); + String endpointPath = contextPath + servletPath + pathMappedEndpoints.getBasePath(); + return !serverContext.getCarrier().getRequestURI().startsWith(endpointPath); + } + return true; + }; + } + + 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(); + if (servletProperties.getPath() == null || "/".equals(servletProperties.getPath())) { + return ""; + } + return servletProperties.getPath(); + } + + } + } 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 939b754424a8..0b2cae448599 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; @@ -53,6 +61,7 @@ * @author Dmytro Nosan * @author Madhura Bhave * @author Moritz Halbritter + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -115,6 +124,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) @@ -142,8 +266,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) { @@ -152,6 +275,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 b996ba206430..66a890218f7d 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; @@ -58,6 +65,7 @@ * @author Madhura Bhave * @author Chanhyeong LEE * @author Moritz Halbritter + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) class WebMvcObservationAutoConfigurationTests { @@ -179,6 +187,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"); } @@ -195,6 +343,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 { diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle index 656ad05a752d..11e604b54a69 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle @@ -11,7 +11,7 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation")) - implementation 'io.micrometer:micrometer-tracing-bridge-brave' +// implementation 'io.micrometer:micrometer-tracing-bridge-brave' runtimeOnly("com.h2database:h2") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 2c35d22ff033..dd7ace1a0463 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -31,3 +31,11 @@ management.endpoints.migrate-legacy-ids=true management.endpoints.jackson.isolated-object-mapper=true spring.jackson.visibility.field=any + +#management.tracing.sampling.probability=1.0 +#management.observations.http.server.actuator.enabled=false +#server.port=8080 +#management.server.port=8888 +#management.endpoints.web.base-path=/mgmt +#spring.mvc.servlet.path=/serv +#server.servlet.context-path=/ctx diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/build.gradle index f0a6461ff720..434ef5e5f46a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/build.gradle @@ -8,6 +8,7 @@ description = "Spring Boot WebFlux smoke test" dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux")) +// implementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation("io.projectreactor:reactor-test") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties index 641c39e65721..a7642d41b361 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux/src/main/resources/application.properties @@ -1,3 +1,11 @@ +spring.application.name=sample management.endpoints.web.exposure.include=* management.endpoints.jackson.isolated-object-mapper=true spring.jackson.visibility.field=any + +#management.tracing.sampling.probability=1.0 +#management.observations.http.server.actuator.enabled=false +#server.port=8080 +#management.server.port=8888 +#management.endpoints.web.base-path=/mgmt +#spring.webflux.base-path=/base