Skip to content

Commit

Permalink
Disable HTTP Observations for Actuator
Browse files Browse the repository at this point in the history
There's something weird with test using WebTestClient,
see the two failing tests in WebFluxObservationAutoConfigurationTests:
- whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath
- whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath

Closes gh-34801
  • Loading branch information
jonatan-ivanov committed Jul 19, 2023
1 parent 4a4b29f commit 8f3f0a3
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,18 +30,21 @@
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.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;

Expand All @@ -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,
Expand Down Expand Up @@ -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;
};

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -30,19 +33,25 @@
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.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;
Expand All @@ -54,14 +63,16 @@
* @author Brian Clozel
* @author Jon Schneider
* @author Dmytro Nosan
* @author Jonatan Ivanov
* @since 3.0.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class })
@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
Expand Down Expand Up @@ -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() : "";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -52,6 +60,7 @@
* @author Brian Clozel
* @author Dmytro Nosan
* @author Madhura Bhave
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
@SuppressWarnings("removal")
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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 {

Expand Down

0 comments on commit 8f3f0a3

Please sign in to comment.