Skip to content

Commit b35e1ad

Browse files
committed
Avoid unbounded metrics creation for requests not handled by Spring MVC
Previously, if an HTTP request that used a templated URI was handled by something other than Spring MVC, a potentially unbounded number of metrics would be created. This happened because, in the absence of Spring MVC's best matching pattern attribute, MetricsFilter would fall back to using the request's path. If the handling route was templated, MetricsFilter would be unaware and would record different metrics for each different path, rather than a single metric for the matching pattern. This cimmit updates MetricsFilter so that it falls back to using unmapped when Spring MVC's best matching pattern attribute is not available. This ensures that an unbounded number of metrics will no longer be created, at the cost of losing specific metrics for requests that are not handled by Spring MVC and that do not use a templated path. Closes gh-5875
1 parent 29e8725 commit b35e1ad

File tree

2 files changed

+35
-24
lines changed

2 files changed

+35
-24
lines changed

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

+5-24
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,9 @@
3535
import org.springframework.core.Ordered;
3636
import org.springframework.core.annotation.Order;
3737
import org.springframework.http.HttpStatus;
38-
import org.springframework.http.HttpStatus.Series;
3938
import org.springframework.util.StopWatch;
4039
import org.springframework.web.filter.OncePerRequestFilter;
4140
import org.springframework.web.servlet.HandlerMapping;
42-
import org.springframework.web.util.UrlPathHelper;
4341

4442
/**
4543
* Filter that counts requests and measures processing times.
@@ -100,7 +98,6 @@ protected void doFilterInternal(HttpServletRequest request,
10098
HttpServletResponse response, FilterChain chain)
10199
throws ServletException, IOException {
102100
StopWatch stopWatch = createStopWatchIfNecessary(request);
103-
String path = new UrlPathHelper().getPathWithinApplication(request);
104101
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
105102
try {
106103
chain.doFilter(request, response);
@@ -113,7 +110,7 @@ protected void doFilterInternal(HttpServletRequest request,
113110
}
114111
stopWatch.stop();
115112
request.removeAttribute(ATTRIBUTE_STOP_WATCH);
116-
recordMetrics(request, path, status, stopWatch.getTotalTimeMillis());
113+
recordMetrics(request, status, stopWatch.getTotalTimeMillis());
117114
}
118115
}
119116
}
@@ -137,27 +134,20 @@ private int getStatus(HttpServletResponse response) {
137134
}
138135
}
139136

140-
private void recordMetrics(HttpServletRequest request, String path, int status,
141-
long time) {
142-
String suffix = determineMetricNameSuffix(request, path, status);
137+
private void recordMetrics(HttpServletRequest request, int status, long time) {
138+
String suffix = determineMetricNameSuffix(request);
143139
submitMetrics(MetricsFilterSubmission.MERGED, request, status, time, suffix);
144140
submitMetrics(MetricsFilterSubmission.PER_HTTP_METHOD, request, status, time,
145141
suffix);
146142
}
147143

148-
private String determineMetricNameSuffix(HttpServletRequest request, String path,
149-
int status) {
144+
private String determineMetricNameSuffix(HttpServletRequest request) {
150145
Object bestMatchingPattern = request
151146
.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
152147
if (bestMatchingPattern != null) {
153148
return fixSpecialCharacters(bestMatchingPattern.toString());
154149
}
155-
Series series = getSeries(status);
156-
if (Series.CLIENT_ERROR.equals(series) || Series.SERVER_ERROR.equals(series)
157-
|| Series.REDIRECTION.equals(series)) {
158-
return UNKNOWN_PATH_SUFFIX;
159-
}
160-
return path;
150+
return UNKNOWN_PATH_SUFFIX;
161151
}
162152

163153
private String fixSpecialCharacters(String value) {
@@ -174,15 +164,6 @@ private String fixSpecialCharacters(String value) {
174164
return result;
175165
}
176166

177-
private Series getSeries(int status) {
178-
try {
179-
return HttpStatus.valueOf(status).series();
180-
}
181-
catch (Exception ex) {
182-
return null;
183-
}
184-
}
185-
186167
private void submitMetrics(MetricsFilterSubmission submission,
187168
HttpServletRequest request, int status, long time, String suffix) {
188169
String prefix = "";

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

+30
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.web.bind.annotation.RestController;
5252
import org.springframework.web.context.request.async.DeferredResult;
5353
import org.springframework.web.filter.OncePerRequestFilter;
54+
import org.springframework.web.servlet.HandlerMapping;
5455
import org.springframework.web.util.NestedServletException;
5556

5657
import static org.assertj.core.api.Assertions.assertThat;
@@ -98,6 +99,8 @@ public void recordsHttpInteractions() throws Exception {
9899
Filter filter = context.getBean(Filter.class);
99100
final MockHttpServletRequest request = new MockHttpServletRequest("GET",
100101
"/test/path");
102+
request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
103+
"/test/path");
101104
final MockHttpServletResponse response = new MockHttpServletResponse();
102105
FilterChain chain = mock(FilterChain.class);
103106
willAnswer(new Answer<Object>() {
@@ -114,6 +117,29 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
114117
context.close();
115118
}
116119

120+
@Test
121+
public void usesUnmappedForInterationsWithNoBestMatchingPattern() throws Exception {
122+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
123+
Config.class, MetricFilterAutoConfiguration.class);
124+
Filter filter = context.getBean(Filter.class);
125+
final MockHttpServletRequest request = new MockHttpServletRequest("GET",
126+
"/test/path");
127+
final MockHttpServletResponse response = new MockHttpServletResponse();
128+
FilterChain chain = mock(FilterChain.class);
129+
willAnswer(new Answer<Object>() {
130+
@Override
131+
public Object answer(InvocationOnMock invocation) throws Throwable {
132+
response.setStatus(200);
133+
return null;
134+
}
135+
}).given(chain).doFilter(request, response);
136+
filter.doFilter(request, response, chain);
137+
verify(context.getBean(CounterService.class)).increment("status.200.unmapped");
138+
verify(context.getBean(GaugeService.class)).submit(eq("response.unmapped"),
139+
anyDouble());
140+
context.close();
141+
}
142+
117143
@Test
118144
public void recordsHttpInteractionsWithTemplateVariable() throws Exception {
119145
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
@@ -362,6 +388,8 @@ public void additionallyRecordsMetricsWithHttpMethodNameIfConfigured()
362388
Filter filter = context.getBean(Filter.class);
363389
final MockHttpServletRequest request = new MockHttpServletRequest("PUT",
364390
"/test/path");
391+
request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
392+
"/test/path");
365393
final MockHttpServletResponse response = new MockHttpServletResponse();
366394
FilterChain chain = mock(FilterChain.class);
367395
willAnswer(new Answer<Object>() {
@@ -419,6 +447,8 @@ public void whenExceptionIsThrownResponseStatusIsUsedWhenResponseHasBeenCommitte
419447
Filter filter = context.getBean(Filter.class);
420448
final MockHttpServletRequest request = new MockHttpServletRequest("GET",
421449
"/test/path");
450+
request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
451+
"/test/path");
422452
final MockHttpServletResponse response = new MockHttpServletResponse();
423453
FilterChain chain = mock(FilterChain.class);
424454
willAnswer(new Answer<Object>() {

0 commit comments

Comments
 (0)