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
pull/35890/head
Moritz Halbritter 1 year ago
parent f562ced79a
commit c73315b4a3

@ -40,7 +40,6 @@ 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.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -79,14 +78,8 @@ public class ObservationAutoConfiguration {
@Bean
@Order(0)
PropertiesObservationFilter propertiesObservationFilter(ObservationProperties properties) {
return new PropertiesObservationFilter(properties);
}
@Bean
@ConditionalOnProperty(name = "management.observations.spring-security.enabled", havingValue = "false")
ObservationPredicate springSecurityObservationsDisabler() {
return (name, context) -> !name.startsWith("spring.security.");
PropertiesObservationFilterPredicate propertiesObservationFilter(ObservationProperties properties) {
return new PropertiesObservationFilterPredicate(properties);
}
@Configuration(proxyBeanMethods = false)

@ -27,6 +27,7 @@ import org.springframework.core.Ordered;
* observations.
*
* @author Brian Clozel
* @author Moritz Halbritter
* @since 3.0.0
*/
@ConfigurationProperties("management.observations")
@ -39,6 +40,20 @@ public class ObservationProperties {
*/
private Map<String, String> keyValues = new LinkedHashMap<>();
/**
* Whether observations starting with the specified name should be enabled. The
* longest match wins, the key 'all' can also be used to configure all observations.
*/
private Map<String, Boolean> enable = new LinkedHashMap<>();
public Map<String, Boolean> getEnable() {
return this.enable;
}
public void setEnable(Map<String, Boolean> enable) {
this.enable = enable;
}
public Http getHttp() {
return this.http;
}

@ -1,51 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.observation;
import java.util.Map.Entry;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation.Context;
import io.micrometer.observation.ObservationFilter;
/**
* {@link ObservationFilter} to apply settings from {@link ObservationProperties}.
*
* @author Moritz Halbritter
*/
class PropertiesObservationFilter implements ObservationFilter {
private final ObservationFilter delegate;
PropertiesObservationFilter(ObservationProperties properties) {
this.delegate = createDelegate(properties);
}
@Override
public Context map(Context context) {
return this.delegate.map(context);
}
private static ObservationFilter createDelegate(ObservationProperties properties) {
if (properties.getKeyValues().isEmpty()) {
return (context) -> context;
}
KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue);
return (context) -> context.addLowCardinalityKeyValues(keyValues);
}
}

@ -0,0 +1,83 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.observation;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Supplier;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation.Context;
import io.micrometer.observation.ObservationFilter;
import io.micrometer.observation.ObservationPredicate;
import org.springframework.util.StringUtils;
/**
* {@link ObservationFilter} to apply settings from {@link ObservationProperties}.
*
* @author Moritz Halbritter
*/
class PropertiesObservationFilterPredicate implements ObservationFilter, ObservationPredicate {
private final ObservationFilter commonKeyValuesFilter;
private final ObservationProperties properties;
PropertiesObservationFilterPredicate(ObservationProperties properties) {
this.properties = properties;
this.commonKeyValuesFilter = createCommonKeyValuesFilter(properties);
}
@Override
public Context map(Context context) {
return this.commonKeyValuesFilter.map(context);
}
@Override
public boolean test(String name, Context context) {
return lookupWithFallbackToAll(this.properties.getEnable(), name, true);
}
private static <T> T lookupWithFallbackToAll(Map<String, T> values, String name, T defaultValue) {
if (values.isEmpty()) {
return defaultValue;
}
return doLookup(values, name, () -> values.getOrDefault("all", defaultValue));
}
private static <T> T doLookup(Map<String, T> values, String name, Supplier<T> defaultValue) {
while (StringUtils.hasLength(name)) {
T result = values.get(name);
if (result != null) {
return result;
}
int lastDot = name.lastIndexOf('.');
name = (lastDot != -1) ? name.substring(0, lastDot) : "";
}
return defaultValue.get();
}
private static ObservationFilter createCommonKeyValuesFilter(ObservationProperties properties) {
if (properties.getKeyValues().isEmpty()) {
return (context) -> context;
}
KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue);
return (context) -> context.addLowCardinalityKeyValues(keyValues);
}
}

@ -2038,12 +2038,6 @@
"level": "error"
}
},
{
"name": "management.observations.spring-security.enabled",
"description": "Whether to enable observations for Spring Security",
"type": "java.lang.Boolean",
"defaultValue": true
},
{
"name": "management.otlp.tracing.compression",
"defaultValue": "none"

@ -191,7 +191,8 @@ class ObservationAutoConfigurationTests {
@Test
void shouldSupplyPropertiesObservationFilterBean() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilter.class));
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilterPredicate.class));
}
@Test
@ -303,14 +304,13 @@ class ObservationAutoConfigurationTests {
@Test
void shouldDisableSpringSecurityObservationsIfPropertyIsSet() {
this.contextRunner.withPropertyValues("management.observations.spring-security.enabled=false")
.run((context) -> {
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation.start("spring.security.filterchains", observationRegistry).stop();
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer())
.isInstanceOf(MeterNotFoundException.class);
});
this.contextRunner.withPropertyValues("management.observations.enable.spring.security=false").run((context) -> {
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation.start("spring.security.filterchains", observationRegistry).stop();
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer())
.isInstanceOf(MeterNotFoundException.class);
});
}
@Configuration(proxyBeanMethods = false)

@ -0,0 +1,100 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.observation;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation.Context;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertiesObservationFilterPredicate}.
*
* @author Moritz Halbritter
*/
class PropertiesObservationFilterPredicateTests {
@Test
void shouldDoNothingIfKeyValuesAreEmpty() {
PropertiesObservationFilterPredicate filter = createFilter();
Context mapped = mapContext(filter, "a", "alpha");
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"));
}
@Test
void shouldAddKeyValues() {
PropertiesObservationFilterPredicate filter = createFilter("b", "beta");
Context mapped = mapContext(filter, "a", "alpha");
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"),
KeyValue.of("b", "beta"));
}
@Test
void shouldFilter() {
PropertiesObservationFilterPredicate predicate = createPredicate("spring.security");
Context context = new Context();
assertThat(predicate.test("spring.security.filterchains", context)).isFalse();
assertThat(predicate.test("spring.security", context)).isFalse();
assertThat(predicate.test("spring.data", context)).isTrue();
assertThat(predicate.test("spring", context)).isTrue();
}
@Test
void filterShouldFallbackToAll() {
PropertiesObservationFilterPredicate predicate = createPredicate("all");
Context context = new Context();
assertThat(predicate.test("spring.security.filterchains", context)).isFalse();
assertThat(predicate.test("spring.security", context)).isFalse();
assertThat(predicate.test("spring.data", context)).isFalse();
assertThat(predicate.test("spring", context)).isFalse();
}
@Test
void shouldNotFilterIfDisabledNamesIsEmpty() {
PropertiesObservationFilterPredicate predicate = createPredicate();
Context context = new Context();
assertThat(predicate.test("spring.security.filterchains", context)).isTrue();
assertThat(predicate.test("spring.security", context)).isTrue();
assertThat(predicate.test("spring.data", context)).isTrue();
assertThat(predicate.test("spring", context)).isTrue();
}
private static Context mapContext(PropertiesObservationFilterPredicate filter, String... initialKeyValues) {
Context context = new Context();
context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues));
return filter.map(context);
}
private static PropertiesObservationFilterPredicate createFilter(String... keyValues) {
ObservationProperties properties = new ObservationProperties();
for (int i = 0; i < keyValues.length; i += 2) {
properties.getKeyValues().put(keyValues[i], keyValues[i + 1]);
}
return new PropertiesObservationFilterPredicate(properties);
}
private static PropertiesObservationFilterPredicate createPredicate(String... disabledNames) {
ObservationProperties properties = new ObservationProperties();
for (String name : disabledNames) {
properties.getEnable().put(name, false);
}
return new PropertiesObservationFilterPredicate(properties);
}
}

@ -1,62 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.observation;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.Observation.Context;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertiesObservationFilter}.
*
* @author Moritz Halbritter
*/
class PropertiesObservationFilterTests {
@Test
void shouldDoNothingIfKeyValuesAreEmpty() {
PropertiesObservationFilter filter = createFilter();
Context mapped = mapContext(filter, "a", "alpha");
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"));
}
@Test
void shouldAddKeyValues() {
PropertiesObservationFilter filter = createFilter("b", "beta");
Context mapped = mapContext(filter, "a", "alpha");
assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"),
KeyValue.of("b", "beta"));
}
private static Context mapContext(PropertiesObservationFilter filter, String... initialKeyValues) {
Context context = new Context();
context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues));
return filter.map(context);
}
private static PropertiesObservationFilter createFilter(String... keyValues) {
ObservationProperties properties = new ObservationProperties();
for (int i = 0; i < keyValues.length; i += 2) {
properties.getKeyValues().put(keyValues[i], keyValues[i + 1]);
}
return new PropertiesObservationFilter(properties);
}
}

@ -40,13 +40,29 @@ The preceding example adds `region` and `stack` key-values to all observations w
[[actuator.observability.preventing-observations]]
=== Preventing Observations
If you'd like to prevent some observations from being reported, you can register beans of type `ObservationPredicate`.
If you'd like to prevent some observations from being reported, you can use the configprop:management.observations.enable[] properties:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
management:
observations:
enable:
denied:
prefix: false
another:
denied:
prefix: false
----
The preceding example will prevent all observations with a name starting with `denied.prefix` or `another.denied.prefix`.
TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.enable.spring.security[] to `false`.
If you need greater control over the prevention of observations, you can register beans of type `ObservationPredicate`.
Observations are only reported if all the `ObservationPredicate` beans return `true` for that observation.
include::code:MyObservationPredicate[]
The preceding example will prevent all observations with a name starting with "denied.prefix.".
TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.spring-security.enabled[] to `false`.
The preceding example will prevent all observations whose name contains "denied".
The next sections will provide more details about logging, metrics and traces.

@ -26,7 +26,7 @@ class MyObservationPredicate implements ObservationPredicate {
@Override
public boolean test(String name, Context context) {
return !name.startsWith("denied.prefix.");
return !name.contains("denied");
}
}

Loading…
Cancel
Save