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..4814aa985796 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 @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.reactive; +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 org.springframework.beans.factory.ObjectProvider; @@ -29,18 +32,22 @@ 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.autoconfigure.web.reactive.WebFluxProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.http.server.reactive.observation.ServerRequestObservationContext; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; import org.springframework.web.filter.reactive.ServerHttpObservationFilter; @@ -51,6 +58,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 +105,33 @@ MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false") + static class ActuatorWebEndpointObservationConfiguration { + + @Bean + ObservationPredicate actuatorWebEndpointObservationPredicate(WebFluxProperties webFluxProperties, + PathMappedEndpoints pathMappedEndpoints) { + return (name, context) -> { + if (context instanceof ServerRequestObservationContext serverContext) { + String endpointPath = getEndpointPath(webFluxProperties, pathMappedEndpoints); + return !serverContext.getCarrier().getURI().getPath().startsWith(endpointPath); + } + return true; + }; + + } + + private static String getEndpointPath(WebFluxProperties webFluxProperties, + PathMappedEndpoints pathMappedEndpoints) { + String webFluxBasePath = getWebFluxBasePath(webFluxProperties); + return Path.of(webFluxBasePath, pathMappedEndpoints.getBasePath()).toString(); + } + + private static String getWebFluxBasePath(WebFluxProperties webFluxProperties) { + return (webFluxProperties.getBasePath() != null) ? webFluxProperties.getBasePath() : ""; + } + + } + } 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..0cc32ccdb512 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,20 +19,30 @@ 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.ReactiveWebServerFactoryAutoConfiguration; 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.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -52,6 +62,7 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -114,6 +125,137 @@ 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"); + }); + } + + /** + * Due to limitations in {@code WebTestClient}, these tests need to start a real + * webserver and utilize a real http client with a real http request. + */ + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath() { + new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) + .with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, + ReactiveWebServerFactoryAutoConfiguration.class, InfoEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + WebFluxEndpointManagementContextConfiguration.class, MetricsAutoConfiguration.class, + ObservationAutoConfiguration.class, WebFluxObservationAutoConfiguration.class)) + .withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withPropertyValues("server.port=0", "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"); + WebTestClient client = createWebTestClientForLocalPort(context); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, client, + "/test-path/test0", "/test-path/actuator/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test-path/test0"); + }); + } + + /** + * Due to limitations in {@code WebTestClient}, these tests need to start a real + * webserver and utilize a real http client with a real http request. + */ + @Test + void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath() { + new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) + .with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, + ReactiveWebServerFactoryAutoConfiguration.class, InfoEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + WebFluxEndpointManagementContextConfiguration.class, MetricsAutoConfiguration.class, + ObservationAutoConfiguration.class, WebFluxObservationAutoConfiguration.class)) + .withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class) + .withPropertyValues("server.port=0", "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"); + WebTestClient client = createWebTestClientForLocalPort(context); + TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, client, + "/test-path/test0", "/test-path/management/info"); + TestObservationRegistryAssert.assertThat(observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1) + .hasAnObservationWithAKeyValue("http.url", "/test-path/test0"); + }); + } + @Test void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) @@ -132,8 +274,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 +283,38 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati return context.getBean(MeterRegistry.class); } + private TestObservationRegistry getInitializedTestObservationRegistry( + AssertableReactiveWebApplicationContext context, String... urls) { + WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient().build(); + return getInitializedTestObservationRegistry(context, client, urls); + } + + private TestObservationRegistry getInitializedTestObservationRegistry( + AssertableReactiveWebApplicationContext context, WebTestClient client, String... urls) { + assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); + for (String url : urls) { + client.get().uri(url).exchange().expectStatus().isOk(); + } + return context.getBean(TestObservationRegistry.class); + } + + private WebTestClient createWebTestClientForLocalPort(AssertableReactiveWebApplicationContext context) { + int port = ((AnnotationConfigReactiveWebServerApplicationContext) context.getSourceApplicationContext()) + .getWebServer() + .getPort(); + return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + } + + @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 { 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..c439eb7dd33d 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