diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java index f74978ddd2..4f2f6edffa 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java @@ -37,8 +37,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; import org.springframework.util.StopWatch; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UrlPathHelper; /** @@ -55,6 +57,7 @@ import org.springframework.web.util.UrlPathHelper; public class MetricFilterAutoConfiguration { private static final int UNDEFINED_HTTP_STATUS = 999; + private static final String UNKNOWN_PATH_SUFFIX = "/unmapped"; @Autowired private CounterService counterService; @@ -86,10 +89,19 @@ public class MetricFilterAutoConfiguration { } finally { stopWatch.stop(); + int status = getStatus(response); + if (request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE) != null) { + suffix = request + .getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE) + .toString().replaceAll("[{}]", "-"); + } + else if (HttpStatus.valueOf(status).is4xxClientError()) { + suffix = UNKNOWN_PATH_SUFFIX; + } String gaugeKey = getKey("response" + suffix); MetricFilterAutoConfiguration.this.gaugeService.submit(gaugeKey, stopWatch.getTotalTimeMillis()); - String counterKey = getKey("status." + getStatus(response) + suffix); + String counterKey = getKey("status." + status + suffix); MetricFilterAutoConfiguration.this.counterService.increment(counterKey); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java index 0f396291d8..5f2ec79bba 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java @@ -27,8 +27,16 @@ import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -36,7 +44,10 @@ import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Matchers.anyDouble; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for {@link MetricFilterAutoConfiguration}. @@ -68,6 +79,57 @@ public class MetricFilterAutoConfigurationTests { context.close(); } + @Test + public void recordsHttpInteractionsWithTemplateVariable() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + Config.class, MetricFilterAutoConfiguration.class); + Filter filter = context.getBean(Filter.class); + MockMvc mvc = MockMvcBuilders.standaloneSetup(new MetricFilterTestController()) + .addFilter(filter).build(); + mvc.perform(get("/templateVarTest/foo")).andExpect(status().isOk()); + + verify(context.getBean(CounterService.class)).increment( + "status.200.templateVarTest.-someVariable-"); + verify(context.getBean(GaugeService.class)).submit( + eq("response.templateVarTest.-someVariable-"), anyDouble()); + context.close(); + } + + @Test + public void recordsKnown404HttpInteractionsAsSingleMetricWithPathAndTemplateVariable() + throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + Config.class, MetricFilterAutoConfiguration.class); + Filter filter = context.getBean(Filter.class); + MockMvc mvc = MockMvcBuilders.standaloneSetup(new MetricFilterTestController()) + .addFilter(filter).build(); + mvc.perform(get("/knownPath/foo")).andExpect(status().isNotFound()); + + verify(context.getBean(CounterService.class)).increment( + "status.404.knownPath.-someVariable-"); + verify(context.getBean(GaugeService.class)).submit( + eq("response.knownPath.-someVariable-"), anyDouble()); + context.close(); + } + + @Test + public void records404HttpInteractionsAsSingleMetric() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + Config.class, MetricFilterAutoConfiguration.class); + Filter filter = context.getBean(Filter.class); + MockMvc mvc = MockMvcBuilders.standaloneSetup(new MetricFilterTestController()) + .addFilter(filter).build(); + mvc.perform(get("/unknownPath/1")).andExpect(status().isNotFound()); + + mvc.perform(get("/unknownPath/2")).andExpect(status().isNotFound()); + + verify(context.getBean(CounterService.class), times(2)).increment( + "status.404.unmapped"); + verify(context.getBean(GaugeService.class), times(2)).submit( + eq("response.unmapped"), anyDouble()); + context.close(); + } + @Test public void skipsFilterIfMissingServices() throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( @@ -88,6 +150,23 @@ public class MetricFilterAutoConfigurationTests { public GaugeService gaugeService() { return mock(GaugeService.class); } + } } + +@RestController +class MetricFilterTestController { + + @RequestMapping("templateVarTest/{someVariable}") + public String testTemplateVariableResolution(@PathVariable String someVariable) { + return someVariable; + } + + @RequestMapping("knownPath/{someVariable}") + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public String testKnownPathWith404Response(@PathVariable String someVariable) { + return someVariable; + } +} \ No newline at end of file