Skip to content

Commit fae5e24

Browse files
Disable HTTP Observations for Actuator
Closes gh-34801
1 parent d1449fb commit fae5e24

File tree

6 files changed

+429
-3
lines changed

6 files changed

+429
-3
lines changed

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

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

1717
package org.springframework.boot.actuate.autoconfigure.observation.web.reactive;
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

2427
import org.springframework.beans.factory.ObjectProvider;
@@ -29,18 +32,22 @@
2932
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
3033
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3134
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
35+
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3236
import org.springframework.boot.autoconfigure.AutoConfiguration;
3337
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3438
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3539
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3640
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
41+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3742
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
43+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
3844
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3945
import org.springframework.context.annotation.Bean;
4046
import org.springframework.context.annotation.Configuration;
4147
import org.springframework.core.Ordered;
4248
import org.springframework.core.annotation.Order;
4349
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention;
50+
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
4451
import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention;
4552
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
4653

@@ -51,6 +58,7 @@
5158
* @author Brian Clozel
5259
* @author Jon Schneider
5360
* @author Dmytro Nosan
61+
* @author Jonatan Ivanov
5462
* @since 3.0.0
5563
*/
5664
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
@@ -97,4 +105,33 @@ MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
97105

98106
}
99107

108+
@Configuration(proxyBeanMethods = false)
109+
@ConditionalOnProperty(value = "management.observations.http.server.actuator.enabled", havingValue = "false")
110+
static class ActuatorWebEndpointObservationConfiguration {
111+
112+
@Bean
113+
ObservationPredicate actuatorWebEndpointObservationPredicate(WebFluxProperties webFluxProperties,
114+
PathMappedEndpoints pathMappedEndpoints) {
115+
return (name, context) -> {
116+
if (context instanceof ServerRequestObservationContext serverContext) {
117+
String endpointPath = getEndpointPath(webFluxProperties, pathMappedEndpoints);
118+
return !serverContext.getCarrier().getURI().getPath().startsWith(endpointPath);
119+
}
120+
return true;
121+
};
122+
123+
}
124+
125+
private static String getEndpointPath(WebFluxProperties webFluxProperties,
126+
PathMappedEndpoints pathMappedEndpoints) {
127+
String webFluxBasePath = getWebFluxBasePath(webFluxProperties);
128+
return Path.of(webFluxBasePath, pathMappedEndpoints.getBasePath()).toString();
129+
}
130+
131+
private static String getWebFluxBasePath(WebFluxProperties webFluxProperties) {
132+
return (webFluxProperties.getBasePath() != null) ? webFluxProperties.getBasePath() : "";
133+
}
134+
135+
}
136+
100137
}

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"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.observation.web.reactive;
18+
19+
import io.micrometer.observation.ObservationPredicate;
20+
import io.micrometer.observation.ObservationRegistry;
21+
import io.micrometer.observation.tck.TestObservationRegistry;
22+
import io.micrometer.observation.tck.TestObservationRegistryAssert;
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
28+
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
29+
import org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration;
30+
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
31+
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
32+
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
33+
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
34+
import org.springframework.boot.actuate.autoconfigure.observation.web.reactive.WebFluxObservationAutoConfigurationIntegrationTests.TestApp;
35+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
36+
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
37+
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
38+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
39+
import org.springframework.boot.test.context.SpringBootTest;
40+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
41+
import org.springframework.boot.test.web.client.TestRestTemplate;
42+
import org.springframework.context.ApplicationContext;
43+
import org.springframework.context.annotation.Bean;
44+
import org.springframework.context.annotation.Configuration;
45+
import org.springframework.http.HttpStatus;
46+
47+
import static org.assertj.core.api.Assertions.assertThat;
48+
49+
/**
50+
* Integration tests for {@link WebFluxObservationAutoConfiguration}. Due to limitations
51+
* in {@code WebTestClient}, these tests need to start a real webserver and utilize a real
52+
* http client.
53+
*
54+
* @author Jonatan Ivanov
55+
*/
56+
@SpringBootTest(
57+
properties = { "spring.main.web-application-type=reactive", "management.endpoints.web.exposure.include=info" },
58+
webEnvironment = WebEnvironment.RANDOM_PORT, classes = { TestApp.class, TestController.class })
59+
class WebFluxObservationAutoConfigurationIntegrationTests {
60+
61+
@Autowired
62+
private TestRestTemplate restTemplate;
63+
64+
@Autowired
65+
private TestObservationRegistry observationRegistry;
66+
67+
@Autowired
68+
private ApplicationContext context;
69+
70+
@AfterEach
71+
void tearDown() {
72+
this.observationRegistry.clear();
73+
}
74+
75+
@Test
76+
void whenAnActuatorEndpointIsCalledObservationsShouldBeRecorded() {
77+
callUrlsToCreateObservations("/test0", "/actuator/info");
78+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
79+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2)
80+
.hasAnObservationWithAKeyValue("http.url", "/test0")
81+
.hasAnObservationWithAKeyValue("http.url", "/actuator/info");
82+
}
83+
84+
@Test
85+
void whenActuatorObservationsEnabledObservationsShouldBeRecorded() {
86+
// "management.observations.http.server.actuator.enabled=true"
87+
callUrlsToCreateObservations("/test0", "/actuator/info");
88+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
89+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 2)
90+
.hasAnObservationWithAKeyValue("http.url", "/test0")
91+
.hasAnObservationWithAKeyValue("http.url", "/actuator/info");
92+
}
93+
94+
@Test
95+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecorded() {
96+
// "management.observations.http.server.actuator.enabled=false"
97+
assertThat(this.context.getBean("actuatorWebEndpointObservationPredicate"))
98+
.isInstanceOf(ObservationPredicate.class);
99+
callUrlsToCreateObservations("/test0", "/actuator/info");
100+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
101+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
102+
.hasAnObservationWithAKeyValue("http.url", "/test0");
103+
}
104+
105+
@Test
106+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomEndpointBasePath() {
107+
// "management.observations.http.server.actuator.enabled=false"
108+
// "management.endpoints.web.base-path=/management"
109+
assertThat(this.context.getBean("actuatorWebEndpointObservationPredicate"))
110+
.isInstanceOf(ObservationPredicate.class);
111+
callUrlsToCreateObservations("/test0", "/management/info");
112+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
113+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
114+
.hasAnObservationWithAKeyValue("http.url", "/test0");
115+
}
116+
117+
@Test
118+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePath() {
119+
// "management.observations.http.server.actuator.enabled=false"
120+
// "spring.webflux.base-path=/test-path"
121+
assertThat(this.context.getBean("actuatorWebEndpointObservationPredicate"))
122+
.isInstanceOf(ObservationPredicate.class);
123+
callUrlsToCreateObservations("/test-path/test0", "/test-path/actuator/info");
124+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
125+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
126+
.hasAnObservationWithAKeyValue("http.url", "/test-path/test0");
127+
}
128+
129+
@Test
130+
void whenActuatorObservationsDisabledObservationsShouldNotBeRecordedUsingCustomWebfluxBasePathAndCustomEndpointBasePath() {
131+
// "management.observations.http.server.actuator.enabled=false"
132+
// "spring.webflux.base-path=/test-path"
133+
// "management.endpoints.web.base-path=/management"
134+
assertThat(this.context.getBean("actuatorWebEndpointObservationPredicate"))
135+
.isInstanceOf(ObservationPredicate.class);
136+
callUrlsToCreateObservations("/test-path/test0", "/test-path/management/info");
137+
TestObservationRegistryAssert.assertThat(this.observationRegistry)
138+
.hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1)
139+
.hasAnObservationWithAKeyValue("http.url", "/test-path/test0");
140+
}
141+
142+
void callUrlsToCreateObservations(String... urls) {
143+
for (String url : urls) {
144+
assertThat(this.restTemplate.getForEntity(url, String.class).getStatusCode()).isEqualTo(HttpStatus.OK);
145+
}
146+
}
147+
148+
@Configuration(proxyBeanMethods = false)
149+
@ImportAutoConfiguration({ ReactiveWebServerFactoryAutoConfiguration.class, WebFluxAutoConfiguration.class,
150+
InfoEndpointAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class,
151+
WebEndpointAutoConfiguration.class, WebFluxEndpointManagementContextConfiguration.class,
152+
MetricsAutoConfiguration.class, ObservationAutoConfiguration.class,
153+
WebFluxObservationAutoConfiguration.class })
154+
static class TestApp {
155+
156+
@Bean
157+
ObservationRegistry observationRegistry() {
158+
return TestObservationRegistry.create();
159+
}
160+
161+
}
162+
163+
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati
132132
return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2");
133133
}
134134

135-
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls)
136-
throws Exception {
135+
private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) {
137136
assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
138137
WebTestClient client = WebTestClient.bindToApplicationContext(context).build();
139138
for (String url : urls) {

0 commit comments

Comments
 (0)