/** * Copyright 2017 Pivotal Software, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.metrics.instrument.web; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.metrics.boot.EnableMetrics; import org.springframework.metrics.instrument.LongTaskTimer; import org.springframework.metrics.instrument.MeterRegistry; import org.springframework.metrics.instrument.Timer; import org.springframework.metrics.annotation.Timed; import org.springframework.metrics.instrument.simple.SimpleMeterRegistry; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = WebmvcMetricsHandlerInterceptorTest.App.class) @WebMvcTest({WebmvcMetricsHandlerInterceptorTest.Controller1.class, WebmvcMetricsHandlerInterceptorTest.Controller2.class}) class WebmvcMetricsHandlerInterceptorTest { @Autowired private MockMvc mvc; @Autowired private SimpleMeterRegistry registry; static CountDownLatch longRequestCountDown = new CountDownLatch(1); @AfterEach void clearRegistry() { registry.clear(); } @Test void metricsGatheredWhenMethodIsTimed() throws Exception { mvc.perform(get("/api/c1/10")).andExpect(status().isOk()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "status", "200", "uri", "api_c1_-id-", "public", "true")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @SuppressWarnings("unchecked") @Test void metricsNotGatheredWhenRequestMappingIsNotTimed() throws Exception { mvc.perform(get("/api/c1/untimed/10")).andExpect(status().isOk()); assertThat(registry.findMeter(Timer.class, "http_server_requests")).isEmpty(); } @Test void metricsGatheredWhenControllerIsTimed() throws Exception { mvc.perform(get("/api/c2/10")).andExpect(status().isOk()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "status", "200")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @Test void metricsGatheredWhenClientRequestBad() throws Exception { mvc.perform(get("/api/c1/oops")).andExpect(status().is4xxClientError()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "status", "400")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @Test void metricsGatheredWhenUnhandledError() throws Exception { assertThatCode(() -> mvc.perform(get("/api/c1/unhandledError/10")).andExpect(status().isOk())) .hasCauseInstanceOf(RuntimeException.class); assertThat(registry.findMeter(Timer.class, "http_server_requests", "exception", "RuntimeException")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @Test void metricsGatheredForLongRunningRequestMapping() throws Exception { MvcResult result = mvc.perform(get("/api/c1/long/10")) .andExpect(request().asyncStarted()) .andReturn(); // while the mapping is running, it contributes to the activeTasks count assertThat(registry.findMeter(LongTaskTimer.class, "my_long_request")) .hasValueSatisfying(t -> assertThat(t.activeTasks()).isEqualTo(1)); // once the mapping completes, we can gather information about status, etc. longRequestCountDown.countDown(); mvc.perform(asyncDispatch(result)).andExpect(status().isOk()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "status", "200")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @Test /* FIXME */ @Disabled("ErrorMvcAutoConfiguration is blowing up on SPEL evaluation of 'timestamp'") void metricsGatheredWhenHandledError() throws Exception { mvc.perform(get("/api/c1/error/10")).andExpect(status().is4xxClientError()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "status", "422")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @Test void metricsGatheredWhenRegexEndpoint() throws Exception { mvc.perform(get("/api/c1/regex/.abc")).andExpect(status().isOk()); assertThat(registry.findMeter(Timer.class, "http_server_requests", "uri", "api_c1_regex_-id-")) .hasValueSatisfying(t -> assertThat(t.count()).isEqualTo(1)); } @SpringBootApplication @EnableMetrics static class App { @Bean MeterRegistry registry() { return new SimpleMeterRegistry(); } } @RestController @RequestMapping("/api/c1") static class Controller1 { @Timed(extraTags = {"public", "true"}) @GetMapping("/{id}") public String successfulWithExtraTags(@PathVariable Long id) { return id.toString(); } @Timed // contains dimensions for status, etc. that can't be known until after the response is sent @Timed(value = "my_long_request", longTask = true) // in progress metric @GetMapping("/long/{id}") public Callable<String> takesLongTimeToSatisfy(@PathVariable Long id) { return () -> { try { longRequestCountDown.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } return id.toString(); }; } @GetMapping("/untimed/{id}") public String successfulButUntimed(@PathVariable Long id) { return id.toString(); } @Timed @GetMapping("/error/{id}") public String alwaysThrowsException(@PathVariable Long id) { throw new IllegalStateException("Boom on $id!"); } @Timed @GetMapping("/unhandledError/{id}") public String alwaysThrowsUnhandledException(@PathVariable Long id) { throw new RuntimeException("Boom on $id!"); } @Timed @GetMapping("/regex/{id:\\.[a-z]+}") public String successfulRegex(@PathVariable String id) { return id; } @ExceptionHandler(value = IllegalStateException.class) @ResponseStatus(code = HttpStatus.UNPROCESSABLE_ENTITY) ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { return new ModelAndView("error"); } } @RestController @Timed @RequestMapping("/api/c2") static class Controller2 { @GetMapping("/{id}") public String successful(@PathVariable Long id) { return id.toString(); } } }