Skip to content

Commit 61fadeb

Browse files
committed
Merge branch '3.4.x' into 3.5.x
Closes gh-47923
2 parents ba9afc0 + 631711f commit 61fadeb

File tree

13 files changed

+208
-45
lines changed

13 files changed

+208
-45
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java

Lines changed: 7 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;
@@ -109,10 +110,13 @@ private void applyCustomizers(MeterRegistry meterRegistry) {
109110
}
110111

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

118122
private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/OnlyOnceLoggingDenyMeterFilter.java

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

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

@@ -28,29 +30,98 @@
2830
import org.springframework.util.Assert;
2931

3032
/**
31-
* {@link MeterFilter} to log only once a warning message and deny a {@link Id Meter.Id}.
33+
* {@link MeterFilter} to log a single warning message and deny a {@link Id Meter.Id}
34+
* after a number of attempts for a given tag.
3235
*
3336
* @author Jon Schneider
3437
* @author Dmytro Nosan
38+
* @author Phillip Webb
3539
* @since 2.0.5
3640
*/
3741
public final class OnlyOnceLoggingDenyMeterFilter implements MeterFilter {
3842

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

4145
private final AtomicBoolean alreadyWarned = new AtomicBoolean();
4246

47+
private final String meterNamePrefix;
48+
49+
private final int maximumTagValues;
50+
51+
private final String tagKey;
52+
4353
private final Supplier<String> message;
4454

55+
private final Set<String> observedTagValues = ConcurrentHashMap.newKeySet();
56+
57+
/**
58+
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} instance.
59+
* @param message the message to log
60+
* @since 2.0.5
61+
* @deprecated since 3.4.12 in favor of
62+
* {@link #OnlyOnceLoggingDenyMeterFilter(String, String, int, String)}
63+
*/
64+
@Deprecated(since = "3.4.12", forRemoval = true)
4565
public OnlyOnceLoggingDenyMeterFilter(Supplier<String> message) {
66+
this(null, null, null, 0, message);
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+
* @since 3.4.12
76+
*/
77+
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues) {
78+
this(meterNamePrefix, tagKey, maximumTagValues, (String) null);
79+
}
80+
81+
/**
82+
* Create a new {@link OnlyOnceLoggingDenyMeterFilter} with an upper bound on the
83+
* number of tags produced by matching metrics.
84+
* @param meterNamePrefix the prefix of the meter name to apply the filter to
85+
* @param tagKey the tag to place an upper bound on
86+
* @param maximumTagValues the total number of tag values that are allowable
87+
* @param hint an additional hint to add to the logged message or {@code null}
88+
* @since 3.4.12
89+
*/
90+
public OnlyOnceLoggingDenyMeterFilter(String meterNamePrefix, String tagKey, int maximumTagValues, String hint) {
91+
this(null, meterNamePrefix, tagKey, maximumTagValues,
92+
() -> String.format("Reached the maximum number of '%s' tags for '%s'.%s", tagKey, meterNamePrefix,
93+
(hint != null) ? " " + hint : ""));
94+
}
95+
96+
private OnlyOnceLoggingDenyMeterFilter(Log logger, String meterNamePrefix, String tagKey, int maximumTagValues,
97+
Supplier<String> message) {
4698
Assert.notNull(message, "'message' must not be null");
99+
Assert.isTrue(maximumTagValues >= 0, "'maximumTagValues' must be positive");
100+
this.logger = (logger != null) ? logger : LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class);
101+
this.meterNamePrefix = meterNamePrefix;
102+
this.maximumTagValues = maximumTagValues;
103+
this.tagKey = tagKey;
47104
this.message = message;
48105
}
49106

50107
@Override
51108
public MeterFilterReply accept(Id id) {
52-
if (logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
53-
logger.warn(this.message.get());
109+
if (this.meterNamePrefix == null) {
110+
return logAndDeny();
111+
}
112+
String tagValue = id.getName().startsWith(this.meterNamePrefix) ? id.getTag(this.tagKey) : null;
113+
if (tagValue != null && !this.observedTagValues.contains(tagValue)) {
114+
if (this.observedTagValues.size() >= this.maximumTagValues) {
115+
return logAndDeny();
116+
}
117+
this.observedTagValues.add(tagValue);
118+
}
119+
return MeterFilterReply.NEUTRAL;
120+
}
121+
122+
private MeterFilterReply logAndDeny() {
123+
if (this.logger.isWarnEnabled() && this.alreadyWarned.compareAndSet(false, true)) {
124+
this.logger.warn(this.message.get());
54125
}
55126
return MeterFilterReply.DENY;
56127
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics.jersey;
1818

19-
import io.micrometer.core.instrument.config.MeterFilter;
2019
import io.micrometer.observation.ObservationRegistry;
2120
import org.glassfish.jersey.micrometer.server.JerseyObservationConvention;
2221
import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener;
@@ -70,12 +69,10 @@ ResourceConfigCustomizer jerseyServerObservationResourceConfigCustomizer(Observa
7069

7170
@Bean
7271
@Order(0)
73-
public MeterFilter jerseyMetricsUriTagFilter(MetricsProperties metricsProperties) {
74-
String metricName = this.observationProperties.getHttp().getServer().getRequests().getName();
75-
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
76-
() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
77-
return MeterFilter.maximumAllowableTags(metricName, "uri",
78-
metricsProperties.getWeb().getServer().getMaxUriTags(), filter);
72+
public OnlyOnceLoggingDenyMeterFilter jerseyMetricsUriTagFilter(MetricsProperties metricsProperties) {
73+
String meterNamePrefix = this.observationProperties.getHttp().getServer().getRequests().getName();
74+
int maximumTagValues = metricsProperties.getWeb().getServer().getMaxUriTags();
75+
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maximumTagValues);
7976
}
8077

8178
}

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.actuate.autoconfigure.observation.web.client;
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

@@ -68,14 +67,13 @@ static class MeterFilterConfiguration {
6867

6968
@Bean
7069
@Order(0)
71-
MeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
70+
OnlyOnceLoggingDenyMeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
7271
MetricsProperties metricsProperties) {
7372
Client clientProperties = metricsProperties.getWeb().getClient();
74-
String name = observationProperties.getHttp().getClient().getRequests().getName();
75-
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(
76-
() -> "Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?"
77-
.formatted(name));
78-
return MeterFilter.maximumAllowableTags(name, "uri", clientProperties.getMaxUriTags(), denyFilter);
73+
String meterNamePrefix = observationProperties.getHttp().getClient().getRequests().getName();
74+
int maxUriTags = clientProperties.getMaxUriTags();
75+
return new OnlyOnceLoggingDenyMeterFilter(meterNamePrefix, "uri", maxUriTags,
76+
"Are you using 'uriVariables'?");
7977
}
8078

8179
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/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.actuate.autoconfigure.observation.web.reactive;
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 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

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/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.actuate.autoconfigure.observation.web.servlet;
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;
@@ -87,13 +86,11 @@ static class MeterFilterConfiguration {
8786

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

9996
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessorTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.micrometer.core.instrument.binder.MeterBinder;
2828
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
2929
import io.micrometer.core.instrument.config.MeterFilter;
30+
import org.assertj.core.api.InstanceOfAssertFactories;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.extension.ExtendWith;
3233
import org.mockito.InOrder;
@@ -124,6 +125,22 @@ void postProcessAndInitializeAppliesFilter() {
124125
then(this.mockConfig).should().meterFilter(this.mockFilter);
125126
}
126127

128+
@Test
129+
void postProcessAndInitializeOnlyAppliesLmiitedFiltersToAutoConfigured() {
130+
OnlyOnceLoggingDenyMeterFilter onlyOnceFilter = mock();
131+
this.filters.add(this.mockFilter);
132+
this.filters.add(onlyOnceFilter);
133+
MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED,
134+
createObjectProvider(this.properties), createObjectProvider(this.customizers),
135+
createObjectProvider(this.filters), createObjectProvider(this.binders));
136+
AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM,
137+
Collections.emptyList());
138+
postProcessAndInitialize(processor, composite);
139+
assertThat(composite).extracting("filters")
140+
.asInstanceOf(InstanceOfAssertFactories.ARRAY)
141+
.containsExactly(onlyOnceFilter);
142+
}
143+
127144
@Test
128145
void postProcessAndInitializeBindsTo() {
129146
given(this.mockRegistry.config()).willReturn(this.mockConfig);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.actuate.autoconfigure.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+
@SuppressWarnings("removal")
40+
void applyWithMssageOnly() {
41+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(() -> "Test");
42+
assertThat(filter.accept(meterId("test", "k", "v"))).isEqualTo(MeterFilterReply.DENY);
43+
}
44+
45+
@Test
46+
void applyWhenNameDoesNotHavePrefixReturnsNeutral() {
47+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
48+
assertThat(filter.accept(meterId("tset", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
49+
assertThat(filter).extracting("observedTagValues").asInstanceOf(InstanceOfAssertFactories.COLLECTION).isEmpty();
50+
}
51+
52+
@Test
53+
void applyWhenNameHasPrefixButNoTagKeyReturnsNeutral() {
54+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
55+
assertThat(filter.accept(meterId("test", "k", "v"))).isEqualTo(MeterFilterReply.NEUTRAL);
56+
assertThat(filter).extracting("observedTagValues")
57+
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
58+
.containsExactly("v");
59+
}
60+
61+
@Test
62+
void applyWhenNameHasPrefixAndTagKeyReturnsNeutralUntilLimit() {
63+
OnlyOnceLoggingDenyMeterFilter filter = new OnlyOnceLoggingDenyMeterFilter("test", "k", 1);
64+
assertThat(filter.accept(meterId("test", "k", "v1"))).isEqualTo(MeterFilterReply.NEUTRAL);
65+
assertThat(filter.accept(meterId("test", "k", "v2"))).isEqualTo(MeterFilterReply.DENY);
66+
assertThat(filter.accept(meterId("test", "k", "v3"))).isEqualTo(MeterFilterReply.DENY);
67+
assertThat(filter).extracting("observedTagValues")
68+
.asInstanceOf(InstanceOfAssertFactories.COLLECTION)
69+
.containsExactly("v1");
70+
}
71+
72+
private Meter.Id meterId(String name, String tagKey, String tagValue) {
73+
MeterRegistry registry = new SimpleMeterRegistry();
74+
Meter meter = Meter.builder(name, Type.COUNTER, Collections.emptyList())
75+
.tag(tagKey, tagValue)
76+
.register(registry);
77+
return meter.getId();
78+
}
79+
80+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
117117
assertThat(registry).hasNumberOfObservationsWithNameEqualTo("http.client.requests", 3);
118118
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
119119
assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2);
120-
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
120+
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.client.requests'.")
121121
.contains("Are you using 'uriVariables'?");
122122
});
123123
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
114114
assertThat(registry).hasNumberOfObservationsWithNameEqualTo("http.client.requests", 3);
115115
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
116116
assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2);
117-
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
117+
assertThat(output).contains("Reached the maximum number of 'uri' tags for 'http.client.requests'.")
118118
.contains("Are you using 'uriVariables'?");
119119
});
120120
}

0 commit comments

Comments
 (0)