Skip to content

Commit 8f3f0a3

Browse files
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
1 parent 4a4b29f commit 8f3f0a3

File tree

5 files changed

+403
-3
lines changed

5 files changed

+403
-3
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java

+22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.micrometer.core.instrument.MeterRegistry;
2020
import io.micrometer.core.instrument.config.MeterFilter;
2121
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationPredicate;
2223
import io.micrometer.observation.ObservationRegistry;
2324

2425
import org.springframework.beans.factory.ObjectProvider;
@@ -29,18 +30,21 @@
2930
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
3031
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3132
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
33+
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3234
import org.springframework.boot.autoconfigure.AutoConfiguration;
3335
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3436
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3537
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3638
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
39+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3740
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
3841
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3942
import org.springframework.context.annotation.Bean;
4043
import org.springframework.context.annotation.Configuration;
4144
import org.springframework.core.Ordered;
4245
import org.springframework.core.annotation.Order;
4346
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention;
47+
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
4448
import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention;
4549
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
4650

@@ -51,6 +55,7 @@
5155
* @author Brian Clozel
5256
* @author Jon Schneider
5357
* @author Dmytro Nosan
58+
* @author Jonatan Ivanov
5459
* @since 3.0.0
5560
*/
5661
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
@@ -97,4 +102,21 @@ MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
97102

98103
}
99104

105+
@Configuration(proxyBeanMethods = false)
106+
@ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false")
107+
static class ActuatorWebEndpointObservationConfiguration {
108+
109+
@Bean
110+
ObservationPredicate actuatorWebEndpointObservationPredicate(PathMappedEndpoints pathMappedEndpoints) {
111+
return (name, context) -> {
112+
if (context instanceof ServerRequestObservationContext serverContext) {
113+
return !serverContext.getCarrier().getURI().getPath().startsWith(pathMappedEndpoints.getBasePath());
114+
}
115+
return true;
116+
};
117+
118+
}
119+
120+
}
121+
100122
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java

+47-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
1818

19+
import java.nio.file.Path;
20+
1921
import io.micrometer.core.instrument.MeterRegistry;
2022
import io.micrometer.core.instrument.config.MeterFilter;
2123
import io.micrometer.observation.Observation;
24+
import io.micrometer.observation.ObservationPredicate;
2225
import io.micrometer.observation.ObservationRegistry;
2326
import jakarta.servlet.DispatcherType;
2427

@@ -30,19 +33,25 @@
3033
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
3134
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3235
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
36+
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3337
import org.springframework.boot.autoconfigure.AutoConfiguration;
3438
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3539
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3640
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
41+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3742
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
43+
import org.springframework.boot.autoconfigure.web.ServerProperties;
44+
import org.springframework.boot.autoconfigure.web.ServerProperties.Servlet;
3845
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
46+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
3947
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4048
import org.springframework.boot.web.servlet.FilterRegistrationBean;
4149
import org.springframework.context.annotation.Bean;
4250
import org.springframework.context.annotation.Configuration;
4351
import org.springframework.core.Ordered;
4452
import org.springframework.core.annotation.Order;
4553
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
54+
import org.springframework.http.server.observation.ServerRequestObservationContext;
4655
import org.springframework.http.server.observation.ServerRequestObservationConvention;
4756
import org.springframework.web.filter.ServerHttpObservationFilter;
4857
import org.springframework.web.servlet.DispatcherServlet;
@@ -54,14 +63,16 @@
5463
* @author Brian Clozel
5564
* @author Jon Schneider
5665
* @author Dmytro Nosan
66+
* @author Jonatan Ivanov
5767
* @since 3.0.0
5868
*/
5969
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
6070
SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class })
6171
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
6272
@ConditionalOnClass({ DispatcherServlet.class, Observation.class })
6373
@ConditionalOnBean(ObservationRegistry.class)
64-
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
74+
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class, ServerProperties.class,
75+
WebMvcProperties.class })
6576
public class WebMvcObservationAutoConfiguration {
6677

6778
@Bean
@@ -97,4 +108,39 @@ MeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationPrope
97108

98109
}
99110

111+
@Configuration(proxyBeanMethods = false)
112+
@ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false")
113+
static class ActuatorWebEndpointObservationConfiguration {
114+
115+
@Bean
116+
ObservationPredicate actuatorWebEndpointObservationPredicate(ServerProperties serverProperties,
117+
WebMvcProperties webMvcProperties, PathMappedEndpoints pathMappedEndpoints) {
118+
return (name, context) -> {
119+
if (context instanceof ServerRequestObservationContext serverContext) {
120+
String endpointPath = getEndpointPath(serverProperties, webMvcProperties, pathMappedEndpoints);
121+
return !serverContext.getCarrier().getRequestURI().startsWith(endpointPath);
122+
}
123+
return true;
124+
};
125+
}
126+
127+
private static String getEndpointPath(ServerProperties serverProperties, WebMvcProperties webMvcProperties,
128+
PathMappedEndpoints pathMappedEndpoints) {
129+
String contextPath = getContextPath(serverProperties);
130+
String servletPath = getServletPath(webMvcProperties);
131+
return Path.of(contextPath, servletPath, pathMappedEndpoints.getBasePath()).toString();
132+
}
133+
134+
private static String getContextPath(ServerProperties serverProperties) {
135+
Servlet servlet = serverProperties.getServlet();
136+
return (servlet.getContextPath() != null) ? servlet.getContextPath() : "";
137+
}
138+
139+
private static String getServletPath(WebMvcProperties webMvcProperties) {
140+
WebMvcProperties.Servlet servletProperties = webMvcProperties.getServlet();
141+
return (servletProperties.getPath() != null) ? servletProperties.getPath() : "";
142+
}
143+
144+
}
145+
100146
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

+6
Original file line numberDiff line numberDiff line change
@@ -2054,6 +2054,12 @@
20542054
"level": "error"
20552055
}
20562056
},
2057+
{
2058+
"name": "management.observations.http.server.actuator.enabled",
2059+
"type": "java.lang.Boolean",
2060+
"description": "Whether to enable HTTP observations for actuator endpoints.",
2061+
"defaultValue": false
2062+
},
20572063
{
20582064
"name": "management.otlp.tracing.compression",
20592065
"defaultValue": "none"

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java

+153-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,23 @@
1919
import java.util.List;
2020

2121
import io.micrometer.core.instrument.MeterRegistry;
22+
import io.micrometer.observation.ObservationRegistry;
23+
import io.micrometer.observation.tck.TestObservationRegistry;
24+
import io.micrometer.observation.tck.TestObservationRegistryAssert;
2225
import org.junit.jupiter.api.Test;
2326
import org.junit.jupiter.api.extension.ExtendWith;
2427
import reactor.core.publisher.Mono;
2528

29+
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
30+
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
31+
import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration;
32+
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
2633
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
2734
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
2835
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
2936
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3037
import org.springframework.boot.autoconfigure.AutoConfigurations;
38+
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
3139
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
3240
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
3341
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@@ -52,6 +60,7 @@
5260
* @author Brian Clozel
5361
* @author Dmytro Nosan
5462
* @author Madhura Bhave
63+
* @author Jonatan Ivanov
5564
*/
5665
@ExtendWith(OutputCaptureExtension.class)
5766
@SuppressWarnings("removal")
@@ -114,6 +123,121 @@ void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(Captu
114123
});
115124
}
116125

126+
@Test
127+
void whenAnActuatorEndpointIsCalledObservationsShouldBeRecorded() {
128+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
129+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
130+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
131+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
132+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
133+
.withPropertyValues("management.endpoints.web.exposure.include=info")
134+
.run((context) -> {
135+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0",
136+
"/actuator/info");
137+
TestObservationRegistryAssert.assertThat(observationRegistry)
138+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2)
139+
.hasAnObservationWithAKeyValue("http.url", "/test0")
140+
.hasAnObservationWithAKeyValue("http.url", "/actuator/info");
141+
});
142+
}
143+
144+
@Test
145+
void whenActuatorObservationsEnabledObservationsShouldBeRecorded() {
146+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
147+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
148+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
149+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
150+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
151+
.withPropertyValues("management.endpoints.web.exposure.include=info",
152+
"management.observations.http.server.actuator.enabled=true")
153+
.run((context) -> {
154+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0",
155+
"/actuator/info");
156+
TestObservationRegistryAssert.assertThat(observationRegistry)
157+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2)
158+
.hasAnObservationWithAKeyValue("http.url", "/test0")
159+
.hasAnObservationWithAKeyValue("http.url", "/actuator/info");
160+
});
161+
}
162+
163+
@Test
164+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecorded() {
165+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
166+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
167+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
168+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
169+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
170+
.withPropertyValues("management.endpoints.web.exposure.include=info",
171+
"management.observations.http.server.actuator.enabled=false")
172+
.run((context) -> {
173+
assertThat(context).hasBean("actuatorWebEndpointObservationPredicate");
174+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0",
175+
"/actuator/info");
176+
TestObservationRegistryAssert.assertThat(observationRegistry)
177+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
178+
.hasAnObservationWithAKeyValue("http.url", "/test0");
179+
});
180+
}
181+
182+
@Test
183+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomEndpointBasePath() {
184+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
185+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
186+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
187+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
188+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
189+
.withPropertyValues("management.endpoints.web.exposure.include=info",
190+
"management.observations.http.server.actuator.enabled=false",
191+
"management.endpoints.web.base-path=/management")
192+
.run((context) -> {
193+
assertThat(context).hasBean("actuatorWebEndpointObservationPredicate");
194+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry(context, "/test0",
195+
"/management/info");
196+
TestObservationRegistryAssert.assertThat(observationRegistry)
197+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
198+
.hasAnObservationWithAKeyValue("http.url", "/test0");
199+
});
200+
}
201+
202+
@Test
203+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath() {
204+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
205+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
206+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
207+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
208+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
209+
.withPropertyValues("management.endpoints.web.exposure.include=info",
210+
"management.observations.http.server.actuator.enabled=false", "spring.webflux.base-path=/test-path")
211+
.run((context) -> {
212+
assertThat(context).hasBean("actuatorWebEndpointObservationPredicate");
213+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-path",
214+
context, "/test0", "/actuator/info");
215+
TestObservationRegistryAssert.assertThat(observationRegistry)
216+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
217+
.hasAnObservationWithAKeyValue("http.url", "/test0");
218+
});
219+
}
220+
221+
@Test
222+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath() {
223+
this.contextRunner.withUserConfiguration(TestController.class, TestObservationRegistryConfiguration.class)
224+
.withConfiguration(AutoConfigurations.of(InfoEndpointAutoConfiguration.class,
225+
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
226+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
227+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class))
228+
.withPropertyValues("management.endpoints.web.exposure.include=info",
229+
"management.observations.http.server.actuator.enabled=false", "spring.webflux.base-path=/test-path",
230+
"management.endpoints.web.base-path=/management")
231+
.run((context) -> {
232+
assertThat(context).hasBean("actuatorWebEndpointObservationPredicate");
233+
TestObservationRegistry observationRegistry = getInitializedTestObservationRegistry("/test-path",
234+
context, "/test0", "/management/info");
235+
TestObservationRegistryAssert.assertThat(observationRegistry)
236+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
237+
.hasAnObservationWithAKeyValue("http.url", "/test0");
238+
});
239+
}
240+
117241
@Test
118242
void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
119243
this.contextRunner.withUserConfiguration(TestController.class)
@@ -132,8 +256,7 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati
132256
return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2");
133257
}
134258

135-
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls)
136-
throws Exception {
259+
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) {
137260
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
138261
WebTestClient client = WebTestClient.bindToApplicationContext(context).build();
139262
for (String url : urls) {
@@ -142,6 +265,34 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati
142265
return context.getBean(MeterRegistry.class);
143266
}
144267

268+
private TestObservationRegistry getInitializedTestObservationRegistry(
269+
AssertableReactiveWebApplicationContext context, String... urls) {
270+
return getInitializedTestObservationRegistry("", context, urls);
271+
}
272+
273+
private TestObservationRegistry getInitializedTestObservationRegistry(String baseUrl,
274+
AssertableReactiveWebApplicationContext context, String... urls) {
275+
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
276+
WebTestClient client = WebTestClient.bindToApplicationContext(context)
277+
.configureClient()
278+
.baseUrl(baseUrl)
279+
.build();
280+
for (String url : urls) {
281+
client.get().uri(url).exchange().expectStatus().isOk();
282+
}
283+
return context.getBean(TestObservationRegistry.class);
284+
}
285+
286+
@Configuration(proxyBeanMethods = false)
287+
static class TestObservationRegistryConfiguration {
288+
289+
@Bean
290+
ObservationRegistry observationRegistry() {
291+
return TestObservationRegistry.create();
292+
}
293+
294+
}
295+
145296
@Configuration(proxyBeanMethods = false)
146297
static class CustomConventionConfiguration {
147298

0 commit comments

Comments
 (0)