Merge branch '3.5.x'

Closes gh-47924
This commit is contained in:
Phillip Webb
2025-11-03 20:41:23 -08:00
10 changed files with 193 additions and 36 deletions
@@ -17,7 +17,6 @@
package org.springframework.boot.http.client.autoconfigure.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -51,13 +50,12 @@ public final class HttpClientMetricsAutoConfiguration {
@Bean
@Order(0)
MeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
OnlyOnceLoggingDenyMeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
MetricsProperties metricsProperties) {
Client clientProperties = metricsProperties.getWeb().getClient();
String name = observationProperties.getHttp().getClient().getRequests().getName();
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(
() -> "Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?".formatted(name));
return MeterFilter.maximumAllowableTags(name, "uri", clientProperties.getMaxUriTags(), denyFilter);
String meterNamePrefix = observationProperties.getHttp().getClient().getRequests().getName();
int maxUriTags = clientProperties.getMaxUriTags();
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags, "Are you using 'uriVariables'?");
}
}
@@ -52,7 +52,7 @@ class HttpClientMetricsAutoConfigurationTests {
meterRegistry.timer("http.client.requests", "uri", "/test/" + i).record(Duration.ofSeconds(1));
}
assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.client.requests'.")
.contains("Are you using 'uriVariables'?");
});
}
@@ -16,6 +16,8 @@
package org.springframework.boot.micrometer.metrics;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
@@ -24,33 +26,90 @@ import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* {@link MeterFilter} to log only once a warning message and deny a {@link Id Meter.Id}.
* {@link MeterFilter} to log a single warning message and deny a {@link Id Meter.Id}
* after a number of attempts for a given tag.
*
* @author Jon Schneider
* @author Dmytro Nosan
* @author Phillip Webb
* @since 4.0.0
*/
public final class OnlyOnceLoggingDenyMeterFilter implements MeterFilter {
private static final Log logger = LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class);
private final Log logger;
private final AtomicBoolean alreadyWarned = new AtomicBoolean();
private final String meterNamePrefix;
private final int maximumTagValues;
private final String tagKey;
private final Supplier<String> message;
public OnlyOnceLoggingDenyMeterFilter(Supplier<String> message) {
private final Set<String> observedTagValues = ConcurrentHashMap.newKeySet();
/**
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} with an upper bound on the
* number of tags produced by matching metrics.
* @param meterNamePrefix the prefix of the meter name to apply the filter to
* @param tagKey the tag to place an upper bound on
* @param maximumTagValues the total number of tag values that are allowable
*/
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues) {
this(meterNamePrefix, tagKey, maximumTagValues, (String) null);
}
/**
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} with an upper bound on the
* number of tags produced by matching metrics.
* @param meterNamePrefix the prefix of the meter name to apply the filter to
* @param tagKey the tag to place an upper bound on
* @param maximumTagValues the total number of tag values that are allowable
* @param hint an additional hint to add to the logged message or {@code null}
*/
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues,
@Nullable String hint) {
this(null, meterNamePrefix, tagKey, maximumTagValues,
() -> String.format("Reached the maximum number of '%s' tags for '%s'.%s", tagKey, meterNamePrefix,
(hint != null) ? " " + hint : ""));
}
private OnlyOnceLoggingDenyMeterFilter(@Nullable Log logger, String meterNamePrefix, String tagKey,
int maximumTagValues, Supplier<String> message) {
Assert.notNull(message, "'message' must not be null");
Assert.isTrue(maximumTagValues >= 0, "'maximumTagValues' must be positive");
this.logger = (logger != null) ? logger : LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class);
this.meterNamePrefix = meterNamePrefix;
this.maximumTagValues = maximumTagValues;
this.tagKey = tagKey;
this.message = message;
}
@Override
public MeterFilterReply accept(Id id) {
if (logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
logger.warn(this.message.get());
if (this.meterNamePrefix == null) {
return logAndDeny();
}
String tagValue = id.getName().startsWith(this.meterNamePrefix) ? id.getTag(this.tagKey) : null;
if (tagValue != null && !this.observedTagValues.contains(tagValue)) {
if (this.observedTagValues.size() >= this.maximumTagValues) {
return logAndDeny();
}
this.observedTagValues.add(tagValue);
}
return MeterFilterReply.NEUTRAL;
}
private MeterFilterReply logAndDeny() {
if (this.logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
this.logger.warn(this.message.get());
}
return MeterFilterReply.DENY;
}
@@ -19,6 +19,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
@@ -30,6 +31,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.micrometer.metrics.OnlyOnceLoggingDenyMeterFilter;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.context.ApplicationContext;
@@ -109,10 +111,13 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing
}
private void applyFilters(MeterRegistry meterRegistry) {
if (meterRegistry instanceof AutoConfiguredCompositeMeterRegistry) {
return;
if (this.filters != null) {
Stream<MeterFilter> filters = this.filters.orderedStream();
if (isAutoConfiguredComposite(meterRegistry)) {
filters = filters.filter(OnlyOnceLoggingDenyMeterFilter.class::isInstance);
}
filters.forEach(meterRegistry.config()::meterFilter);
}
this.filters.orderedStream().forEach(meterRegistry.config()::meterFilter);
}
private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) {
@@ -0,0 +1,73 @@
/*
* Copyright 2012-present 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.micrometer.metrics;
import java.util.Collections;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.Meter.Type;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilterReply;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OnlyOnceLoggingDenyMeterFilter}.
*
* @author Phillip Webb
*/
class OnlyOnceLoggingDenyMeterFilterTests {
@Test
void applyWhenNameDoesNotHavePrefixReturnsNeutral() {
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
assertThat(filter.accept(meterId("tset", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
assertThat(filter).extracting("observedTagValues").asInstanceOf(InstanceOfAssertFactories.COLLECTION).isEmpty();
}
@Test
void applyWhenNameHasPrefixButNoTagKeyReturnsNeutral() {
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
assertThat(filter.accept(meterId("test", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
assertThat(filter).extracting("observedTagValues")
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
.containsExactly("v");
}
@Test
void applyWhenNameHasPrefixAndTagKeyReturnsNeutralUntilLimit() {
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
assertThat(filter.accept(meterId("test", "k", "v1"))).isEqualTo(MeterFilterReply.NEUTRAL);
assertThat(filter.accept(meterId("test", "k", "v2"))).isEqualTo(MeterFilterReply.DENY);
assertThat(filter.accept(meterId("test", "k", "v3"))).isEqualTo(MeterFilterReply.DENY);
assertThat(filter).extracting("observedTagValues")
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
.containsExactly("v1");
}
private Meter.Id meterId(String name, String tagKey, String tagValue) {
MeterRegistry registry = new SimpleMeterRegistry();
Meter meter = Meter.builder(name, Type.COUNTER, Collections.emptyList())
.tag(tagKey, tagValue)
.register(registry);
return meter.getId();
}
}
@@ -19,6 +19,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.MeterRegistry;
@@ -27,6 +28,7 @@ import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
@@ -36,6 +38,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.micrometer.metrics.OnlyOnceLoggingDenyMeterFilter;
import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryPostProcessor.CompositeMeterRegistries;
import static org.assertj.core.api.Assertions.assertThat;
@@ -131,6 +134,22 @@ class MeterRegistryPostProcessorTests {
then(this.mockConfig).should().meterFilter(this.mockFilter);
}
@Test
void postProcessAndInitializeOnlyAppliesLmiitedFiltersToAutoConfigured() {
OnlyOnceLoggingDenyMeterFilter onlyOnceFilter = mock();
this.filters.add(this.mockFilter);
this.filters.add(onlyOnceFilter);
MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED,
createObjectProvider(this.properties), createObjectProvider(this.customizers),
createObjectProvider(this.filters), createObjectProvider(this.binders));
AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM,
Collections.emptyList());
postProcessAndInitialize(processor, composite);
assertThat(composite).extracting("filters")
.asInstanceOf(InstanceOfAssertFactories.ARRAY)
.containsExactly(onlyOnceFilter);
}
@Test
void postProcessAndInitializeBindsTo() {
given(this.mockRegistry.config()).willReturn(this.mockConfig);
@@ -273,11 +292,18 @@ class MeterRegistryPostProcessorTests {
}
private <T> ObjectProvider<T> createEmptyObjectProvider() {
return new ObjectProvider<T>() {
return new ObjectProvider<>() {
@Override
public T getObject() throws BeansException {
throw new NoSuchBeanDefinitionException("No bean");
}
@Override
public Stream<T> orderedStream() {
return Stream.empty();
}
};
}
@@ -17,7 +17,6 @@
package org.springframework.boot.webflux.autoconfigure;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
@@ -64,12 +63,10 @@ public final class WebFluxObservationAutoConfiguration {
@Bean
@Order(0)
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
String name = this.observationProperties.getHttp().getServer().getRequests().getName();
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
() -> "Reached the maximum number of URI tags for '%s'.".formatted(name));
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
filter);
OnlyOnceLoggingDenyMeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
String meterNamePrefix = this.observationProperties.getHttp().getServer().getRequests().getName();
int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags();
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags);
}
@Bean
@@ -66,7 +66,7 @@ class WebFluxObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'");
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.server.requests'");
});
}
@@ -80,7 +80,7 @@ class WebFluxObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context, "my.http.server.requests");
assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'my.http.server.requests'");
});
}
@@ -93,7 +93,8 @@ class WebFluxObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.server.requests").meters()).hasSize(3);
assertThat(output).doesNotContain("Reached the maximum number of URI tags for 'http.server.requests'");
assertThat(output)
.doesNotContain("Reached the maximum number of 'uri' tags for 'http.server.requests'");
});
}
@@ -17,7 +17,6 @@
package org.springframework.boot.webmvc.autoconfigure;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.DispatcherType;
@@ -86,13 +85,11 @@ public final class WebMvcObservationAutoConfiguration {
@Bean
@Order(0)
MeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationProperties,
OnlyOnceLoggingDenyMeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationProperties,
MetricsProperties metricsProperties) {
String name = observationProperties.getHttp().getServer().getRequests().getName();
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
() -> String.format("Reached the maximum number of URI tags for '%s'.", name));
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
filter);
String meterNamePrefix = observationProperties.getHttp().getServer().getRequests().getName();
int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags();
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags);
}
}
@@ -139,7 +139,7 @@ class WebMvcObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'");
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.server.requests'");
});
}
@@ -153,7 +153,7 @@ class WebMvcObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'my.http.server.requests'");
});
}
@@ -166,7 +166,8 @@ class WebMvcObservationAutoConfigurationTests {
.run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.server.requests").meters()).hasSize(3);
assertThat(output).doesNotContain("Reached the maximum number of URI tags for 'http.server.requests'");
assertThat(output)
.doesNotContain("Reached the maximum number of 'uri' tags for 'http.server.requests'");
});
}