Skip to content

Commit 7849474

Browse files
committed
Merge branch '3.5.x'
Closes gh-47924
2 parents 75cfe82 + 61fadeb commit 7849474

File tree

10 files changed

+193
-36
lines changed

10 files changed

+193
-36
lines changed

module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/autoconfigure/metrics/HttpClientMetricsAutoConfiguration.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.http.client.autoconfigure.metrics;
1818

1919
import io.micrometer.core.instrument.MeterRegistry;
20-
import io.micrometer.core.instrument.config.MeterFilter;
2120

2221
import org.springframework.boot.autoconfigure.AutoConfiguration;
2322
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -51,13 +50,12 @@ public final class HttpClientMetricsAutoConfiguration {
5150

5251
@Bean
5352
@Order(0)
54-
MeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
53+
OnlyOnceLoggingDenyMeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
5554
MetricsProperties metricsProperties) {
5655
Client clientProperties = metricsProperties.getWeb().getClient();
57-
String name = observationProperties.getHttp().getClient().getRequests().getName();
58-
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(
59-
() -> "Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?".formatted(name));
60-
return MeterFilter.maximumAllowableTags(name, "uri", clientProperties.getMaxUriTags(), denyFilter);
56+
String meterNamePrefix = observationProperties.getHttp().getClient().getRequests().getName();
57+
int maxUriTags = clientProperties.getMaxUriTags();
58+
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags, "Are you using 'uriVariables'?");
6159
}
6260

6361
}

module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/autoconfigure/metrics/HttpClientMetricsAutoConfigurationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
5252
meterRegistry.timer("http.client.requests", "uri", "/test/" + i).record(Duration.ofSeconds(1));
5353
}
5454
assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2);
55-
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
55+
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.client.requests'.")
5656
.contains("Are you using 'uriVariables'?");
5757
});
5858
}

module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/OnlyOnceLoggingDenyMeterFilter.java

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.micrometer.metrics;
1818

19+
import java.util.Set;
20+
import java.util.concurrent.ConcurrentHashMap;
1921
import java.util.concurrent.atomic.AtomicBoolean;
2022
import java.util.function.Supplier;
2123

@@ -24,33 +26,90 @@
2426
import io.micrometer.core.instrument.config.MeterFilterReply;
2527
import org.apache.commons.logging.Log;
2628
import org.apache.commons.logging.LogFactory;
29+
import org.jspecify.annotations.Nullable;
2730

2831
import org.springframework.util.Assert;
2932

3033
/**
31-
* {@link MeterFilter} to log only once a warning message and deny a {@link Id Meter.Id}.
34+
* {@link MeterFilter} to log a single warning message and deny a {@link Id Meter.Id}
35+
* after a number of attempts for a given tag.
3236
*
3337
* @author Jon Schneider
3438
* @author Dmytro Nosan
39+
* @author Phillip Webb
3540
* @since 4.0.0
3641
*/
3742
public final class OnlyOnceLoggingDenyMeterFilter implements MeterFilter {
3843

39-
private static final Log logger = LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class);
44+
private final Log logger;
4045

4146
private final AtomicBoolean alreadyWarned = new AtomicBoolean();
4247

48+
private final String meterNamePrefix;
49+
50+
private final int maximumTagValues;
51+
52+
private final String tagKey;
53+
4354
private final Supplier<String> message;
4455

45-
public OnlyOnceLoggingDenyMeterFilter(Supplier<String> message) {
56+
private final Set<String> observedTagValues = ConcurrentHashMap.newKeySet();
57+
58+
/**
59+
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} with an upper bound on the
60+
* number of tags produced by matching metrics.
61+
* @param meterNamePrefix the prefix of the meter name to apply the filter to
62+
* @param tagKey the tag to place an upper bound on
63+
* @param maximumTagValues the total number of tag values that are allowable
64+
*/
65+
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues) {
66+
this(meterNamePrefix, tagKey, maximumTagValues, (String) null);
67+
}
68+
69+
/**
70+
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} with an upper bound on the
71+
* number of tags produced by matching metrics.
72+
* @param meterNamePrefix the prefix of the meter name to apply the filter to
73+
* @param tagKey the tag to place an upper bound on
74+
* @param maximumTagValues the total number of tag values that are allowable
75+
* @param hint an additional hint to add to the logged message or {@code null}
76+
*/
77+
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues,
78+
@Nullable String hint) {
79+
this(null, meterNamePrefix, tagKey, maximumTagValues,
80+
() -> String.format("Reached the maximum number of '%s' tags for '%s'.%s", tagKey, meterNamePrefix,
81+
(hint != null) ? " " + hint : ""));
82+
}
83+
84+
private OnlyOnceLoggingDenyMeterFilter(@Nullable Log logger, String meterNamePrefix, String tagKey,
85+
int maximumTagValues, Supplier<String> message) {
4686
Assert.notNull(message, "'message' must not be null");
87+
Assert.isTrue(maximumTagValues >= 0, "'maximumTagValues' must be positive");
88+
this.logger = (logger != null) ? logger : LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class);
89+
this.meterNamePrefix = meterNamePrefix;
90+
this.maximumTagValues = maximumTagValues;
91+
this.tagKey = tagKey;
4792
this.message = message;
4893
}
4994

5095
@Override
5196
public MeterFilterReply accept(Id id) {
52-
if (logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
53-
logger.warn(this.message.get());
97+
if (this.meterNamePrefix == null) {
98+
return logAndDeny();
99+
}
100+
String tagValue = id.getName().startsWith(this.meterNamePrefix) ? id.getTag(this.tagKey) : null;
101+
if (tagValue != null && !this.observedTagValues.contains(tagValue)) {
102+
if (this.observedTagValues.size() >= this.maximumTagValues) {
103+
return logAndDeny();
104+
}
105+
this.observedTagValues.add(tagValue);
106+
}
107+
return MeterFilterReply.NEUTRAL;
108+
}
109+
110+
private MeterFilterReply logAndDeny() {
111+
if (this.logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
112+
this.logger.warn(this.message.get());
54113
}
55114
return MeterFilterReply.DENY;
56115
}

module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.LinkedHashSet;
2020
import java.util.List;
2121
import java.util.Set;
22+
import java.util.stream.Stream;
2223

2324
import io.micrometer.core.instrument.MeterRegistry;
2425
import io.micrometer.core.instrument.Metrics;
@@ -30,6 +31,7 @@
3031
import org.springframework.beans.factory.ObjectProvider;
3132
import org.springframework.beans.factory.SmartInitializingSingleton;
3233
import org.springframework.beans.factory.config.BeanPostProcessor;
34+
import org.springframework.boot.micrometer.metrics.OnlyOnceLoggingDenyMeterFilter;
3335
import org.springframework.boot.util.LambdaSafe;
3436
import org.springframework.context.ApplicationContext;
3537

@@ -109,10 +111,13 @@ private void applyCustomizers(MeterRegistry meterRegistry) {
109111
}
110112

111113
private void applyFilters(MeterRegistry meterRegistry) {
112-
if (meterRegistry instanceof AutoConfiguredCompositeMeterRegistry) {
113-
return;
114+
if (this.filters != null) {
115+
Stream<MeterFilter> filters = this.filters.orderedStream();
116+
if (isAutoConfiguredComposite(meterRegistry)) {
117+
filters = filters.filter(OnlyOnceLoggingDenyMeterFilter.class::isInstance);
118+
}
119+
filters.forEach(meterRegistry.config()::meterFilter);
114120
}
115-
this.filters.orderedStream().forEach(meterRegistry.config()::meterFilter);
116121
}
117122

118123
private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2012-present 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.micrometer.metrics;
18+
19+
import java.util.Collections;
20+
21+
import io.micrometer.core.instrument.Meter;
22+
import io.micrometer.core.instrument.Meter.Type;
23+
import io.micrometer.core.instrument.MeterRegistry;
24+
import io.micrometer.core.instrument.config.MeterFilterReply;
25+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
26+
import org.assertj.core.api.InstanceOfAssertFactories;
27+
import org.junit.jupiter.api.Test;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Tests for {@link OnlyOnceLoggingDenyMeterFilter}.
33+
*
34+
* @author Phillip Webb
35+
*/
36+
class OnlyOnceLoggingDenyMeterFilterTests {
37+
38+
@Test
39+
void applyWhenNameDoesNotHavePrefixReturnsNeutral() {
40+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
41+
assertThat(filter.accept(meterId("tset", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
42+
assertThat(filter).extracting("observedTagValues").asInstanceOf(InstanceOfAssertFactories.COLLECTION).isEmpty();
43+
}
44+
45+
@Test
46+
void applyWhenNameHasPrefixButNoTagKeyReturnsNeutral() {
47+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
48+
assertThat(filter.accept(meterId("test", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
49+
assertThat(filter).extracting("observedTagValues")
50+
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
51+
.containsExactly("v");
52+
}
53+
54+
@Test
55+
void applyWhenNameHasPrefixAndTagKeyReturnsNeutralUntilLimit() {
56+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
57+
assertThat(filter.accept(meterId("test", "k", "v1"))).isEqualTo(MeterFilterReply.NEUTRAL);
58+
assertThat(filter.accept(meterId("test", "k", "v2"))).isEqualTo(MeterFilterReply.DENY);
59+
assertThat(filter.accept(meterId("test", "k", "v3"))).isEqualTo(MeterFilterReply.DENY);
60+
assertThat(filter).extracting("observedTagValues")
61+
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
62+
.containsExactly("v1");
63+
}
64+
65+
private Meter.Id meterId(String name, String tagKey, String tagValue) {
66+
MeterRegistry registry = new SimpleMeterRegistry();
67+
Meter meter = Meter.builder(name, Type.COUNTER, Collections.emptyList())
68+
.tag(tagKey, tagValue)
69+
.register(registry);
70+
return meter.getId();
71+
}
72+
73+
}

module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.stream.Stream;
2223

2324
import io.micrometer.core.instrument.Clock;
2425
import io.micrometer.core.instrument.MeterRegistry;
@@ -27,6 +28,7 @@
2728
import io.micrometer.core.instrument.binder.MeterBinder;
2829
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
2930
import io.micrometer.core.instrument.config.MeterFilter;
31+
import org.assertj.core.api.InstanceOfAssertFactories;
3032
import org.junit.jupiter.api.Test;
3133
import org.junit.jupiter.api.extension.ExtendWith;
3234
import org.mockito.InOrder;
@@ -36,6 +38,7 @@
3638
import org.springframework.beans.BeansException;
3739
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3840
import org.springframework.beans.factory.ObjectProvider;
41+
import org.springframework.boot.micrometer.metrics.OnlyOnceLoggingDenyMeterFilter;
3942
import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryPostProcessor.CompositeMeterRegistries;
4043

4144
import static org.assertj.core.api.Assertions.assertThat;
@@ -131,6 +134,22 @@ void postProcessAndInitializeAppliesFilter() {
131134
then(this.mockConfig).should().meterFilter(this.mockFilter);
132135
}
133136

137+
@Test
138+
void postProcessAndInitializeOnlyAppliesLmiitedFiltersToAutoConfigured() {
139+
OnlyOnceLoggingDenyMeterFilter onlyOnceFilter = mock();
140+
this.filters.add(this.mockFilter);
141+
this.filters.add(onlyOnceFilter);
142+
MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED,
143+
createObjectProvider(this.properties), createObjectProvider(this.customizers),
144+
createObjectProvider(this.filters), createObjectProvider(this.binders));
145+
AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM,
146+
Collections.emptyList());
147+
postProcessAndInitialize(processor, composite);
148+
assertThat(composite).extracting("filters")
149+
.asInstanceOf(InstanceOfAssertFactories.ARRAY)
150+
.containsExactly(onlyOnceFilter);
151+
}
152+
134153
@Test
135154
void postProcessAndInitializeBindsTo() {
136155
given(this.mockRegistry.config()).willReturn(this.mockConfig);
@@ -273,11 +292,18 @@ private <T> ObjectProvider<T> createObjectProvider(T object) {
273292
}
274293

275294
private <T> ObjectProvider<T> createEmptyObjectProvider() {
276-
return new ObjectProvider<T>() {
295+
return new ObjectProvider<>() {
296+
277297
@Override
278298
public T getObject() throws BeansException {
279299
throw new NoSuchBeanDefinitionException("No bean");
280300
}
301+
302+
@Override
303+
public Stream<T> orderedStream() {
304+
return Stream.empty();
305+
}
306+
281307
};
282308
}
283309

module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfiguration.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.webflux.autoconfigure;
1818

1919
import io.micrometer.core.instrument.MeterRegistry;
20-
import io.micrometer.core.instrument.config.MeterFilter;
2120
import io.micrometer.observation.Observation;
2221
import io.micrometer.observation.ObservationRegistry;
2322

@@ -64,12 +63,10 @@ public final class WebFluxObservationAutoConfiguration {
6463

6564
@Bean
6665
@Order(0)
67-
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
68-
String name = this.observationProperties.getHttp().getServer().getRequests().getName();
69-
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
70-
() -> "Reached the maximum number of URI tags for '%s'.".formatted(name));
71-
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
72-
filter);
66+
OnlyOnceLoggingDenyMeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
67+
String meterNamePrefix = this.observationProperties.getHttp().getServer().getRequests().getName();
68+
int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags();
69+
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags);
7370
}
7471

7572
@Bean

module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/observation/autoconfigure/WebFluxObservationAutoConfigurationTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
6666
.run((context) -> {
6767
MeterRegistry registry = getInitializedMeterRegistry(context);
6868
assertThat(registry.get("http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
69-
assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'");
69+
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.server.requests'");
7070
});
7171
}
7272

@@ -80,7 +80,7 @@ void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(Captu
8080
.run((context) -> {
8181
MeterRegistry registry = getInitializedMeterRegistry(context, "my.http.server.requests");
8282
assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
83-
assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
83+
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'my.http.server.requests'");
8484
});
8585
}
8686

@@ -93,7 +93,8 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
9393
.run((context) -> {
9494
MeterRegistry registry = getInitializedMeterRegistry(context);
9595
assertThat(registry.get("http.server.requests").meters()).hasSize(3);
96-
assertThat(output).doesNotContain("Reached the maximum number of URI tags for 'http.server.requests'");
96+
assertThat(output)
97+
.doesNotContain("Reached the maximum number of 'uri' tags for 'http.server.requests'");
9798
});
9899
}
99100

module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfiguration.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.webmvc.autoconfigure;
1818

1919
import io.micrometer.core.instrument.MeterRegistry;
20-
import io.micrometer.core.instrument.config.MeterFilter;
2120
import io.micrometer.observation.Observation;
2221
import io.micrometer.observation.ObservationRegistry;
2322
import jakarta.servlet.DispatcherType;
@@ -86,13 +85,11 @@ static class MeterFilterConfiguration {
8685

8786
@Bean
8887
@Order(0)
89-
MeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationProperties,
88+
OnlyOnceLoggingDenyMeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationProperties,
9089
MetricsProperties metricsProperties) {
91-
String name = observationProperties.getHttp().getServer().getRequests().getName();
92-
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
93-
() -> String.format("Reached the maximum number of URI tags for '%s'.", name));
94-
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
95-
filter);
90+
String meterNamePrefix = observationProperties.getHttp().getServer().getRequests().getName();
91+
int maxUriTags = metricsProperties.getWeb().getServer().getMaxUriTags();
92+
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags);
9693
}
9794

9895
}

0 commit comments

Comments
 (0)