Skip to content

Commit b8b4ea4

Browse files
committed
Improve MetricsFilter’s handling of async requests
Previously, MetricsFilter would treat async requests the same as sync requests and would record their response status as soon as the request had been processed by the filter chain. This would result in a 200 response being recorded and the actual response status produced by the async processing being ignored. Furthermore, the time that was recorded for the request would not include the time take for any async processing. This commit updates MetricsFilter to check whether or not an async request has been started before it records the metrics for the request. In the event of an async request having been started no metrics are recorded. Instead, the StopWatch is stored in a request attribute that is then retrieved during the filtering of the request’s async dispatch(es). Once the async processing has completed (isAsyncStarted() returns false), the StopWatch is stopped, removed from the request’s attributes and the metrics for the request are recorded. Closes gh-4098
1 parent a94f3a0 commit b8b4ea4

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsFilter.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
@Order(Ordered.HIGHEST_PRECEDENCE)
4343
final class MetricsFilter extends OncePerRequestFilter {
4444

45+
private static final String ATTRIBUTE_STOP_WATCH = MetricsFilter.class.getName()
46+
+ ".StopWatch";
47+
4548
private static final int UNDEFINED_HTTP_STATUS = 999;
4649

4750
private static final String UNKNOWN_PATH_SUFFIX = "/unmapped";
@@ -57,22 +60,39 @@ public MetricsFilter(CounterService counterService, GaugeService gaugeService) {
5760
this.gaugeService = gaugeService;
5861
}
5962

63+
@Override
64+
protected boolean shouldNotFilterAsyncDispatch() {
65+
return false;
66+
}
67+
6068
@Override
6169
protected void doFilterInternal(HttpServletRequest request,
6270
HttpServletResponse response, FilterChain chain) throws ServletException,
6371
IOException {
64-
StopWatch stopWatch = new StopWatch();
65-
stopWatch.start();
72+
StopWatch stopWatch = createStopWatchIfNecessary(request);
6673
String path = new UrlPathHelper().getPathWithinApplication(request);
6774
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
6875
try {
6976
chain.doFilter(request, response);
7077
status = getStatus(response);
7178
}
7279
finally {
73-
stopWatch.stop();
74-
recordMetrics(request, path, status, stopWatch.getTotalTimeMillis());
80+
if (!request.isAsyncStarted()) {
81+
stopWatch.stop();
82+
request.removeAttribute(ATTRIBUTE_STOP_WATCH);
83+
recordMetrics(request, path, status, stopWatch.getTotalTimeMillis());
84+
}
85+
}
86+
}
87+
88+
private StopWatch createStopWatchIfNecessary(HttpServletRequest request) {
89+
StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
90+
if (stopWatch == null) {
91+
stopWatch = new StopWatch();
92+
stopWatch.start();
93+
request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
7594
}
95+
return stopWatch;
7696
}
7797

7898
private int getStatus(HttpServletResponse response) {

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java

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

1919
import java.io.IOException;
20+
import java.util.concurrent.CountDownLatch;
2021

2122
import javax.servlet.Filter;
2223
import javax.servlet.FilterChain;
@@ -34,21 +35,28 @@
3435
import org.springframework.context.annotation.Configuration;
3536
import org.springframework.core.annotation.Order;
3637
import org.springframework.http.HttpStatus;
38+
import org.springframework.http.ResponseEntity;
3739
import org.springframework.mock.web.MockHttpServletRequest;
3840
import org.springframework.mock.web.MockHttpServletResponse;
3941
import org.springframework.stereotype.Component;
4042
import org.springframework.test.web.servlet.MockMvc;
43+
import org.springframework.test.web.servlet.MvcResult;
4144
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
4245
import org.springframework.web.bind.annotation.PathVariable;
4346
import org.springframework.web.bind.annotation.RequestMapping;
4447
import org.springframework.web.bind.annotation.ResponseBody;
4548
import org.springframework.web.bind.annotation.ResponseStatus;
4649
import org.springframework.web.bind.annotation.RestController;
50+
import org.springframework.web.context.request.async.DeferredResult;
4751
import org.springframework.web.filter.OncePerRequestFilter;
4852
import org.springframework.web.util.NestedServletException;
4953

5054
import static org.hamcrest.Matchers.equalTo;
55+
import static org.hamcrest.Matchers.is;
56+
import static org.hamcrest.Matchers.notNullValue;
57+
import static org.hamcrest.Matchers.nullValue;
5158
import static org.junit.Assert.assertThat;
59+
import static org.junit.Assert.fail;
5260
import static org.mockito.BDDMockito.willAnswer;
5361
import static org.mockito.BDDMockito.willThrow;
5462
import static org.mockito.Matchers.anyDouble;
@@ -57,7 +65,10 @@
5765
import static org.mockito.Mockito.mock;
5866
import static org.mockito.Mockito.times;
5967
import static org.mockito.Mockito.verify;
68+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
6069
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
70+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
71+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
6172
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6273

6374
/**
@@ -202,6 +213,56 @@ public void gaugeServiceThatThrows() throws Exception {
202213
context.close();
203214
}
204215

216+
@Test
217+
public void correctlyRecordsMetricsForDeferredResultResponse() throws Exception {
218+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
219+
Config.class, MetricFilterAutoConfiguration.class);
220+
MetricsFilter filter = context.getBean(MetricsFilter.class);
221+
CountDownLatch latch = new CountDownLatch(1);
222+
MockMvc mvc = MockMvcBuilders
223+
.standaloneSetup(new MetricFilterTestController(latch)).addFilter(filter)
224+
.build();
225+
String attributeName = MetricsFilter.class.getName() + ".StopWatch";
226+
MvcResult result = mvc.perform(post("/create")).andExpect(status().isOk())
227+
.andExpect(request().asyncStarted())
228+
.andExpect(request().attribute(attributeName, is(notNullValue())))
229+
.andReturn();
230+
latch.countDown();
231+
mvc.perform(asyncDispatch(result)).andExpect(status().isCreated())
232+
.andExpect(request().attribute(attributeName, is(nullValue())));
233+
verify(context.getBean(CounterService.class)).increment("status.201.create");
234+
context.close();
235+
}
236+
237+
@Test
238+
public void correctlyRecordsMetricsForFailedDeferredResultResponse() throws Exception {
239+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
240+
Config.class, MetricFilterAutoConfiguration.class);
241+
MetricsFilter filter = context.getBean(MetricsFilter.class);
242+
CountDownLatch latch = new CountDownLatch(1);
243+
MockMvc mvc = MockMvcBuilders
244+
.standaloneSetup(new MetricFilterTestController(latch)).addFilter(filter)
245+
.build();
246+
String attributeName = MetricsFilter.class.getName() + ".StopWatch";
247+
MvcResult result = mvc.perform(post("/createFailure")).andExpect(status().isOk())
248+
.andExpect(request().asyncStarted())
249+
.andExpect(request().attribute(attributeName, is(notNullValue())))
250+
.andReturn();
251+
latch.countDown();
252+
try {
253+
mvc.perform(asyncDispatch(result));
254+
fail();
255+
}
256+
catch (Exception ex) {
257+
assertThat(result.getRequest().getAttribute(attributeName), is(nullValue()));
258+
verify(context.getBean(CounterService.class)).increment(
259+
"status.500.createFailure");
260+
}
261+
finally {
262+
context.close();
263+
}
264+
}
265+
205266
@Configuration
206267
public static class Config {
207268

@@ -220,6 +281,16 @@ public GaugeService gaugeService() {
220281
@RestController
221282
class MetricFilterTestController {
222283

284+
private final CountDownLatch latch;
285+
286+
MetricFilterTestController() {
287+
this(null);
288+
}
289+
290+
MetricFilterTestController(CountDownLatch latch) {
291+
this.latch = latch;
292+
}
293+
223294
@RequestMapping("templateVarTest/{someVariable}")
224295
public String testTemplateVariableResolution(@PathVariable String someVariable) {
225296
return someVariable;
@@ -237,6 +308,43 @@ public String testKnownPathWith404Response(@PathVariable String someVariable) {
237308
public String testException() {
238309
throw new RuntimeException();
239310
}
311+
312+
@RequestMapping("create")
313+
public DeferredResult<ResponseEntity<String>> create() {
314+
final DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>();
315+
new Thread(new Runnable() {
316+
@Override
317+
public void run() {
318+
try {
319+
MetricFilterTestController.this.latch.await();
320+
result.setResult(new ResponseEntity<String>("Done",
321+
HttpStatus.CREATED));
322+
}
323+
catch (InterruptedException ex) {
324+
}
325+
}
326+
}).start();
327+
return result;
328+
}
329+
330+
@RequestMapping("createFailure")
331+
public DeferredResult<ResponseEntity<String>> createFailure() {
332+
final DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>();
333+
new Thread(new Runnable() {
334+
@Override
335+
public void run() {
336+
try {
337+
MetricFilterTestController.this.latch.await();
338+
result.setErrorResult(new Exception("It failed"));
339+
}
340+
catch (InterruptedException ex) {
341+
342+
}
343+
}
344+
}).start();
345+
return result;
346+
}
347+
240348
}
241349

242350
@Component

0 commit comments

Comments
 (0)