Skip to content

Commit 936c31c

Browse files
committed
Prevent metrics failures from adversely affecting web responses
Fixes gh-24151
1 parent 678916f commit 936c31c

File tree

7 files changed

+202
-34
lines changed

7 files changed

+202
-34
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
2020

2121
import io.micrometer.core.instrument.MeterRegistry;
2222
import io.micrometer.core.instrument.Tag;
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
2325
import org.reactivestreams.Publisher;
2426
import reactor.core.publisher.Mono;
2527

@@ -41,6 +43,8 @@
4143
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
4244
public class MetricsWebFilter implements WebFilter {
4345

46+
private static Log logger = LogFactory.getLog(MetricsWebFilter.class);
47+
4448
private final MeterRegistry registry;
4549

4650
private final WebFluxTagsProvider tagsProvider;
@@ -97,9 +101,15 @@ private void onError(ServerWebExchange exchange, long start, Throwable cause) {
97101
}
98102

99103
private void record(ServerWebExchange exchange, long start, Throwable cause) {
100-
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
101-
this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(System.nanoTime() - start,
102-
TimeUnit.NANOSECONDS);
104+
try {
105+
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
106+
this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(System.nanoTime() - start,
107+
TimeUnit.NANOSECONDS);
108+
}
109+
catch (Exception ex) {
110+
logger.warn("Failed to record timer metrics", ex);
111+
// Allow exchange to continue, unaffected by metrics problem
112+
}
103113
}
104114

105115
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,8 @@
3030
import io.micrometer.core.instrument.LongTaskTimer;
3131
import io.micrometer.core.instrument.MeterRegistry;
3232
import io.micrometer.core.instrument.Tag;
33+
import org.apache.commons.logging.Log;
34+
import org.apache.commons.logging.LogFactory;
3335

3436
import org.springframework.core.annotation.MergedAnnotationCollectors;
3537
import org.springframework.core.annotation.MergedAnnotations;
@@ -46,6 +48,8 @@
4648
*/
4749
public class LongTaskTimingHandlerInterceptor implements HandlerInterceptor {
4850

51+
private static final Log logger = LogFactory.getLog(LongTaskTimingHandlerInterceptor.class);
52+
4953
private final MeterRegistry registry;
5054

5155
private final WebMvcTagsProvider tagsProvider;
@@ -90,12 +94,18 @@ private void startAndAttachTimingContext(HttpServletRequest request, Object hand
9094
private Collection<LongTaskTimer.Sample> getLongTaskTimerSamples(HttpServletRequest request, Object handler,
9195
Set<Timed> annotations) {
9296
List<LongTaskTimer.Sample> samples = new ArrayList<>();
93-
annotations.stream().filter(Timed::longTask).forEach((annotation) -> {
94-
Iterable<Tag> tags = this.tagsProvider.getLongRequestTags(request, handler);
95-
LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags);
96-
LongTaskTimer timer = builder.register(this.registry);
97-
samples.add(timer.start());
98-
});
97+
try {
98+
annotations.stream().filter(Timed::longTask).forEach((annotation) -> {
99+
Iterable<Tag> tags = this.tagsProvider.getLongRequestTags(request, handler);
100+
LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags);
101+
LongTaskTimer timer = builder.register(this.registry);
102+
samples.add(timer.start());
103+
});
104+
}
105+
catch (Exception ex) {
106+
logger.warn("Failed to start long task timers", ex);
107+
// Allow request-response exchange to continue, unaffected by metrics problem
108+
}
99109
return samples;
100110
}
101111

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,8 @@
3131
import io.micrometer.core.instrument.Timer;
3232
import io.micrometer.core.instrument.Timer.Builder;
3333
import io.micrometer.core.instrument.Timer.Sample;
34+
import org.apache.commons.logging.Log;
35+
import org.apache.commons.logging.LogFactory;
3436

3537
import org.springframework.boot.actuate.metrics.AutoTimer;
3638
import org.springframework.core.annotation.MergedAnnotationCollectors;
@@ -53,6 +55,8 @@
5355
*/
5456
public class WebMvcMetricsFilter extends OncePerRequestFilter {
5557

58+
private static final Log logger = LogFactory.getLog(WebMvcMetricsFilter.class);
59+
5660
private final MeterRegistry registry;
5761

5862
private final WebMvcTagsProvider tagsProvider;
@@ -120,21 +124,27 @@ private TimingContext startAndAttachTimingContext(HttpServletRequest request) {
120124

121125
private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,
122126
Throwable exception) {
123-
Object handler = getHandler(request);
124-
Set<Timed> annotations = getTimedAnnotations(handler);
125-
Timer.Sample timerSample = timingContext.getTimerSample();
126-
if (annotations.isEmpty()) {
127-
if (this.autoTimer.isEnabled()) {
128-
Builder builder = this.autoTimer.builder(this.metricName);
129-
timerSample.stop(getTimer(builder, handler, request, response, exception));
127+
try {
128+
Object handler = getHandler(request);
129+
Set<Timed> annotations = getTimedAnnotations(handler);
130+
Timer.Sample timerSample = timingContext.getTimerSample();
131+
if (annotations.isEmpty()) {
132+
if (this.autoTimer.isEnabled()) {
133+
Builder builder = this.autoTimer.builder(this.metricName);
134+
timerSample.stop(getTimer(builder, handler, request, response, exception));
135+
}
130136
}
131-
}
132-
else {
133-
for (Timed annotation : annotations) {
134-
Builder builder = Timer.builder(annotation, this.metricName);
135-
timerSample.stop(getTimer(builder, handler, request, response, exception));
137+
else {
138+
for (Timed annotation : annotations) {
139+
Builder builder = Timer.builder(annotation, this.metricName);
140+
timerSample.stop(getTimer(builder, handler, request, response, exception));
141+
}
136142
}
137143
}
144+
catch (Exception ex) {
145+
logger.warn("Failed to record timer metrics", ex);
146+
// Allow request-response exchange to continue, unaffected by metrics problem
147+
}
138148
}
139149

140150
private Object getHandler(HttpServletRequest request) {

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,8 +17,10 @@
1717
package org.springframework.boot.actuate.metrics.web.reactive.server;
1818

1919
import java.time.Duration;
20+
import java.util.concurrent.atomic.AtomicBoolean;
2021

2122
import io.micrometer.core.instrument.MockClock;
23+
import io.micrometer.core.instrument.Tag;
2224
import io.micrometer.core.instrument.simple.SimpleConfig;
2325
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
2426
import org.junit.jupiter.api.BeforeEach;
@@ -29,6 +31,7 @@
2931
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
3032
import org.springframework.mock.web.server.MockServerWebExchange;
3133
import org.springframework.web.reactive.HandlerMapping;
34+
import org.springframework.web.server.ServerWebExchange;
3235
import org.springframework.web.util.pattern.PathPatternParser;
3336

3437
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,6 +46,8 @@ class MetricsWebFilterTests {
4346

4447
private static final String REQUEST_METRICS_NAME = "http.server.requests";
4548

49+
private final FaultyWebFluxTagsProvider tagsProvider = new FaultyWebFluxTagsProvider();
50+
4651
private SimpleMeterRegistry registry;
4752

4853
private MetricsWebFilter webFilter;
@@ -51,7 +56,7 @@ class MetricsWebFilterTests {
5156
void setup() {
5257
MockClock clock = new MockClock();
5358
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock);
54-
this.webFilter = new MetricsWebFilter(this.registry, new DefaultWebFluxTagsProvider(true), REQUEST_METRICS_NAME,
59+
this.webFilter = new MetricsWebFilter(this.registry, this.tagsProvider, REQUEST_METRICS_NAME,
5560
AutoTimer.ENABLED);
5661
}
5762

@@ -116,6 +121,14 @@ void trailingSlashShouldNotRecordDuplicateMetrics() {
116121
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("status", "200").timer().count()).isEqualTo(2);
117122
}
118123

124+
@Test
125+
void whenMetricsRecordingFailsThenExchangeFilteringSucceeds() {
126+
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
127+
this.tagsProvider.failOnce();
128+
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
129+
.block(Duration.ofSeconds(30));
130+
}
131+
119132
private MockServerWebExchange createExchange(String path, String pathPattern) {
120133
PathPatternParser parser = new PathPatternParser();
121134
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build());
@@ -127,4 +140,26 @@ private void assertMetricsContainsTag(String tagKey, String tagValue) {
127140
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1);
128141
}
129142

143+
class FaultyWebFluxTagsProvider extends DefaultWebFluxTagsProvider {
144+
145+
private volatile AtomicBoolean fail = new AtomicBoolean(false);
146+
147+
FaultyWebFluxTagsProvider() {
148+
super(true);
149+
}
150+
151+
@Override
152+
public Iterable<Tag> httpRequestTags(ServerWebExchange exchange, Throwable exception) {
153+
if (this.fail.compareAndSet(true, false)) {
154+
throw new RuntimeException();
155+
}
156+
return super.httpRequestTags(exchange, exception);
157+
}
158+
159+
void failOnce() {
160+
this.fail.set(true);
161+
}
162+
163+
}
164+
130165
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2012-2021 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.metrics.web.servlet;
18+
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
21+
import javax.servlet.http.HttpServletRequest;
22+
import javax.servlet.http.HttpServletResponse;
23+
24+
import io.micrometer.core.instrument.Tag;
25+
26+
/**
27+
* {@link WebMvcTagsProvider} used for testing that can be configured to fail when getting
28+
* tags or long task tags.
29+
*
30+
* @author Andy Wilkinson
31+
*/
32+
class FaultyWebMvcTagsProvider extends DefaultWebMvcTagsProvider {
33+
34+
private volatile AtomicBoolean fail = new AtomicBoolean(false);
35+
36+
FaultyWebMvcTagsProvider() {
37+
super(true);
38+
}
39+
40+
@Override
41+
public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
42+
Throwable exception) {
43+
if (this.fail.compareAndSet(true, false)) {
44+
throw new RuntimeException();
45+
}
46+
return super.getTags(request, response, handler, exception);
47+
}
48+
49+
@Override
50+
public Iterable<Tag> getLongRequestTags(HttpServletRequest request, Object handler) {
51+
if (this.fail.compareAndSet(true, false)) {
52+
throw new RuntimeException();
53+
}
54+
return super.getLongRequestTags(request, handler);
55+
}
56+
57+
void failOnce() {
58+
this.fail.set(true);
59+
}
60+
61+
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.concurrent.Callable;
2020
import java.util.concurrent.CompletableFuture;
2121
import java.util.concurrent.CyclicBarrier;
22+
import java.util.concurrent.TimeUnit;
2223
import java.util.concurrent.atomic.AtomicReference;
2324

2425
import io.micrometer.core.annotation.Timed;
@@ -76,6 +77,9 @@ class LongTaskTimingHandlerInterceptorTests {
7677
@Autowired
7778
private CyclicBarrier callableBarrier;
7879

80+
@Autowired
81+
private FaultyWebMvcTagsProvider tagsProvider;
82+
7983
private MockMvc mvc;
8084

8185
@BeforeEach
@@ -117,6 +121,26 @@ void asyncCallableRequest() throws Exception {
117121
.isEqualTo(0);
118122
}
119123

124+
@Test
125+
void whenMetricsRecordingFailsResponseIsUnaffected() throws Exception {
126+
this.tagsProvider.failOnce();
127+
AtomicReference<MvcResult> result = new AtomicReference<>();
128+
Thread backgroundRequest = new Thread(() -> {
129+
try {
130+
result.set(
131+
this.mvc.perform(get("/api/c1/callable/10")).andExpect(request().asyncStarted()).andReturn());
132+
}
133+
catch (Exception ex) {
134+
fail("Failed to execute async request", ex);
135+
}
136+
});
137+
backgroundRequest.start();
138+
this.callableBarrier.await(10, TimeUnit.SECONDS);
139+
this.callableBarrier.await(10, TimeUnit.SECONDS);
140+
backgroundRequest.join();
141+
this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk());
142+
}
143+
120144
@Configuration(proxyBeanMethods = false)
121145
@EnableWebMvc
122146
@Import(Controller1.class)
@@ -138,13 +162,17 @@ CyclicBarrier callableBarrier() {
138162
}
139163

140164
@Bean
141-
WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry) {
165+
FaultyWebMvcTagsProvider webMvcTagsProvider() {
166+
return new FaultyWebMvcTagsProvider();
167+
}
168+
169+
@Bean
170+
WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry, WebMvcTagsProvider tagsProvider) {
142171
return new WebMvcConfigurer() {
143172

144173
@Override
145174
public void addInterceptors(InterceptorRegistry registry) {
146-
registry.addInterceptor(
147-
new LongTaskTimingHandlerInterceptor(meterRegistry, new DefaultWebMvcTagsProvider()));
175+
registry.addInterceptor(new LongTaskTimingHandlerInterceptor(meterRegistry, tagsProvider));
148176
}
149177

150178
};

0 commit comments

Comments
 (0)