Skip to content

Commit c73315b

Browse files
committed
Add property to prevent observations starting with a prefix
For example, setting management.observations.enable.denied.prefix=false will prevent all observations starting with 'denied.prefix' Closes gh-34802
1 parent f562ced commit c73315b

File tree

10 files changed

+230
-142
lines changed

10 files changed

+230
-142
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
4141
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4242
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
43-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4443
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4544
import org.springframework.context.annotation.Bean;
4645
import org.springframework.context.annotation.Configuration;
@@ -79,14 +78,8 @@ ObservationRegistry observationRegistry() {
7978

8079
@Bean
8180
@Order(0)
82-
PropertiesObservationFilter propertiesObservationFilter(ObservationProperties properties) {
83-
return new PropertiesObservationFilter(properties);
84-
}
85-
86-
@Bean
87-
@ConditionalOnProperty(name = "management.observations.spring-security.enabled", havingValue = "false")
88-
ObservationPredicate springSecurityObservationsDisabler() {
89-
return (name, context) -> !name.startsWith("spring.security.");
81+
PropertiesObservationFilterPredicate propertiesObservationFilter(ObservationProperties properties) {
82+
return new PropertiesObservationFilterPredicate(properties);
9083
}
9184

9285
@Configuration(proxyBeanMethods = false)

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* observations.
2828
*
2929
* @author Brian Clozel
30+
* @author Moritz Halbritter
3031
* @since 3.0.0
3132
*/
3233
@ConfigurationProperties("management.observations")
@@ -39,6 +40,20 @@ public class ObservationProperties {
3940
*/
4041
private Map<String, String> keyValues = new LinkedHashMap<>();
4142

43+
/**
44+
* Whether observations starting with the specified name should be enabled. The
45+
* longest match wins, the key 'all' can also be used to configure all observations.
46+
*/
47+
private Map<String, Boolean> enable = new LinkedHashMap<>();
48+
49+
public Map<String, Boolean> getEnable() {
50+
return this.enable;
51+
}
52+
53+
public void setEnable(Map<String, Boolean> enable) {
54+
this.enable = enable;
55+
}
56+
4257
public Http getHttp() {
4358
return this.http;
4459
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java

Lines changed: 0 additions & 51 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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;
18+
19+
import java.util.Map;
20+
import java.util.Map.Entry;
21+
import java.util.function.Supplier;
22+
23+
import io.micrometer.common.KeyValues;
24+
import io.micrometer.observation.Observation.Context;
25+
import io.micrometer.observation.ObservationFilter;
26+
import io.micrometer.observation.ObservationPredicate;
27+
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
* {@link ObservationFilter} to apply settings from {@link ObservationProperties}.
32+
*
33+
* @author Moritz Halbritter
34+
*/
35+
class PropertiesObservationFilterPredicate implements ObservationFilter, ObservationPredicate {
36+
37+
private final ObservationFilter commonKeyValuesFilter;
38+
39+
private final ObservationProperties properties;
40+
41+
PropertiesObservationFilterPredicate(ObservationProperties properties) {
42+
this.properties = properties;
43+
this.commonKeyValuesFilter = createCommonKeyValuesFilter(properties);
44+
}
45+
46+
@Override
47+
public Context map(Context context) {
48+
return this.commonKeyValuesFilter.map(context);
49+
}
50+
51+
@Override
52+
public boolean test(String name, Context context) {
53+
return lookupWithFallbackToAll(this.properties.getEnable(), name, true);
54+
}
55+
56+
private static <T> T lookupWithFallbackToAll(Map<String, T> values, String name, T defaultValue) {
57+
if (values.isEmpty()) {
58+
return defaultValue;
59+
}
60+
return doLookup(values, name, () -> values.getOrDefault("all", defaultValue));
61+
}
62+
63+
private static <T> T doLookup(Map<String, T> values, String name, Supplier<T> defaultValue) {
64+
while (StringUtils.hasLength(name)) {
65+
T result = values.get(name);
66+
if (result != null) {
67+
return result;
68+
}
69+
int lastDot = name.lastIndexOf('.');
70+
name = (lastDot != -1) ? name.substring(0, lastDot) : "";
71+
}
72+
return defaultValue.get();
73+
}
74+
75+
private static ObservationFilter createCommonKeyValuesFilter(ObservationProperties properties) {
76+
if (properties.getKeyValues().isEmpty()) {
77+
return (context) -> context;
78+
}
79+
KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue);
80+
return (context) -> context.addLowCardinalityKeyValues(keyValues);
81+
}
82+
83+
}

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,12 +2038,6 @@
20382038
"level": "error"
20392039
}
20402040
},
2041-
{
2042-
"name": "management.observations.spring-security.enabled",
2043-
"description": "Whether to enable observations for Spring Security",
2044-
"type": "java.lang.Boolean",
2045-
"defaultValue": true
2046-
},
20472041
{
20482042
"name": "management.otlp.tracing.compression",
20492043
"defaultValue": "none"

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ void autoConfiguresObservationFilters() {
191191

192192
@Test
193193
void shouldSupplyPropertiesObservationFilterBean() {
194-
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilter.class));
194+
this.contextRunner
195+
.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilterPredicate.class));
195196
}
196197

197198
@Test
@@ -303,14 +304,13 @@ void shouldNotDisableSpringSecurityObservationsByDefault() {
303304

304305
@Test
305306
void shouldDisableSpringSecurityObservationsIfPropertyIsSet() {
306-
this.contextRunner.withPropertyValues("management.observations.spring-security.enabled=false")
307-
.run((context) -> {
308-
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
309-
Observation.start("spring.security.filterchains", observationRegistry).stop();
310-
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
311-
assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer())
312-
.isInstanceOf(MeterNotFoundException.class);
313-
});
307+
this.contextRunner.withPropertyValues("management.observations.enable.spring.security=false").run((context) -> {
308+
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
309+
Observation.start("spring.security.filterchains", observationRegistry).stop();
310+
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
311+
assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer())
312+
.isInstanceOf(MeterNotFoundException.class);
313+
});
314314
}
315315

316316
@Configuration(proxyBeanMethods = false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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;
18+
19+
import io.micrometer.common.KeyValue;
20+
import io.micrometer.common.KeyValues;
21+
import io.micrometer.observation.Observation.Context;
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
/**
27+
* Tests for {@link PropertiesObservationFilterPredicate}.
28+
*
29+
* @author Moritz Halbritter
30+
*/
31+
class PropertiesObservationFilterPredicateTests {
32+
33+
@Test
34+
void shouldDoNothingIfKeyValuesAreEmpty() {
35+
PropertiesObservationFilterPredicate filter = createFilter();
36+
Context mapped = mapContext(filter, "a", "alpha");
37+
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"));
38+
}
39+
40+
@Test
41+
void shouldAddKeyValues() {
42+
PropertiesObservationFilterPredicate filter = createFilter("b", "beta");
43+
Context mapped = mapContext(filter, "a", "alpha");
44+
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"),
45+
KeyValue.of("b", "beta"));
46+
}
47+
48+
@Test
49+
void shouldFilter() {
50+
PropertiesObservationFilterPredicate predicate = createPredicate("spring.security");
51+
Context context = new Context();
52+
assertThat(predicate.test("spring.security.filterchains", context)).isFalse();
53+
assertThat(predicate.test("spring.security", context)).isFalse();
54+
assertThat(predicate.test("spring.data", context)).isTrue();
55+
assertThat(predicate.test("spring", context)).isTrue();
56+
}
57+
58+
@Test
59+
void filterShouldFallbackToAll() {
60+
PropertiesObservationFilterPredicate predicate = createPredicate("all");
61+
Context context = new Context();
62+
assertThat(predicate.test("spring.security.filterchains", context)).isFalse();
63+
assertThat(predicate.test("spring.security", context)).isFalse();
64+
assertThat(predicate.test("spring.data", context)).isFalse();
65+
assertThat(predicate.test("spring", context)).isFalse();
66+
}
67+
68+
@Test
69+
void shouldNotFilterIfDisabledNamesIsEmpty() {
70+
PropertiesObservationFilterPredicate predicate = createPredicate();
71+
Context context = new Context();
72+
assertThat(predicate.test("spring.security.filterchains", context)).isTrue();
73+
assertThat(predicate.test("spring.security", context)).isTrue();
74+
assertThat(predicate.test("spring.data", context)).isTrue();
75+
assertThat(predicate.test("spring", context)).isTrue();
76+
}
77+
78+
private static Context mapContext(PropertiesObservationFilterPredicate filter, String... initialKeyValues) {
79+
Context context = new Context();
80+
context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues));
81+
return filter.map(context);
82+
}
83+
84+
private static PropertiesObservationFilterPredicate createFilter(String... keyValues) {
85+
ObservationProperties properties = new ObservationProperties();
86+
for (int i = 0; i < keyValues.length; i += 2) {
87+
properties.getKeyValues().put(keyValues[i], keyValues[i + 1]);
88+
}
89+
return new PropertiesObservationFilterPredicate(properties);
90+
}
91+
92+
private static PropertiesObservationFilterPredicate createPredicate(String... disabledNames) {
93+
ObservationProperties properties = new ObservationProperties();
94+
for (String name : disabledNames) {
95+
properties.getEnable().put(name, false);
96+
}
97+
return new PropertiesObservationFilterPredicate(properties);
98+
}
99+
100+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)