/*
* Copyright 2012-2017 the original author or authors.
*
* 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.boot.actuate.autoconfigure;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.boot.actuate.metrics.CounterService;
import org.springframework.boot.actuate.metrics.GaugeService;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
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 org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.NestedServletException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
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.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link MetricFilterAutoConfiguration}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public class MetricFilterAutoConfigurationTests {
@Test
public void defaultMetricFilterAutoConfigurationProperties() {
MetricFilterProperties properties = new MetricFilterProperties();
assertThat(properties.getGaugeSubmissions())
.containsExactly(MetricsFilterSubmission.MERGED);
assertThat(properties.getCounterSubmissions())
.containsExactly(MetricsFilterSubmission.MERGED);
}
@Test
public void recordsHttpInteractions() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class);
Filter filter = context.getBean(Filter.class);
final MockHttpServletRequest request = new MockHttpServletRequest("GET",
"/test/path");
final MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
response.setStatus(200);
return null;
}
}).given(chain).doFilter(request, response);
filter.doFilter(request, response, chain);
verify(context.getBean(CounterService.class)).increment("status.200.test.path");
verify(context.getBean(GaugeService.class)).submit(eq("response.test.path"),
anyDouble());
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 recordsHttpInteractionsWithRegexTemplateVariable() 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("/templateVarRegexTest/foo")).andExpect(status().isOk());
verify(context.getBean(CounterService.class))
.increment("status.200.templateVarRegexTest.someVariable");
verify(context.getBean(GaugeService.class))
.submit(eq("response.templateVarRegexTest.someVariable"), anyDouble());
context.close();
}
@Test
public void recordsHttpInteractionsWithWildcardMapping() 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("/wildcardMapping/foo")).andExpect(status().isOk());
verify(context.getBean(CounterService.class))
.increment("status.200.wildcardMapping.star");
verify(context.getBean(GaugeService.class))
.submit(eq("response.wildcardMapping.star"), anyDouble());
context.close();
}
@Test
public void recordsHttpInteractionsWithDoubleWildcardMapping() 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("/doubleWildcardMapping/foo/bar/baz")).andExpect(status().isOk());
verify(context.getBean(CounterService.class))
.increment("status.200.doubleWildcardMapping.star-star.baz");
verify(context.getBean(GaugeService.class))
.submit(eq("response.doubleWildcardMapping.star-star.baz"), 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 records302HttpInteractionsAsSingleMetric() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class, RedirectFilter.class);
MetricsFilter filter = context.getBean(MetricsFilter.class);
MockMvc mvc = MockMvcBuilders.standaloneSetup(new MetricFilterTestController())
.addFilter(filter).addFilter(context.getBean(RedirectFilter.class))
.build();
mvc.perform(get("/unknownPath/1")).andExpect(status().is3xxRedirection());
mvc.perform(get("/unknownPath/2")).andExpect(status().is3xxRedirection());
verify(context.getBean(CounterService.class), times(2))
.increment("status.302.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(
MetricFilterAutoConfiguration.class);
assertThat(context.getBeansOfType(Filter.class).size()).isEqualTo(0);
context.close();
}
@Test
public void skipsFilterIfPropertyDisabled() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
EnvironmentTestUtils.addEnvironment(context,
"endpoints.metrics.filter.enabled:false");
context.register(Config.class, MetricFilterAutoConfiguration.class);
context.refresh();
assertThat(context.getBeansOfType(Filter.class).size()).isEqualTo(0);
context.close();
}
@Test
public void controllerMethodThatThrowsUnhandledException() 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();
try {
mvc.perform(get("/unhandledException"))
.andExpect(status().isInternalServerError());
}
catch (NestedServletException ex) {
// Expected
}
verify(context.getBean(CounterService.class))
.increment("status.500.unhandledException");
verify(context.getBean(GaugeService.class))
.submit(eq("response.unhandledException"), anyDouble());
context.close();
}
@Test
public void gaugeServiceThatThrows() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class);
GaugeService gaugeService = context.getBean(GaugeService.class);
willThrow(new IllegalStateException()).given(gaugeService).submit(anyString(),
anyDouble());
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 correctlyRecordsMetricsForDeferredResultResponse() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class);
MetricsFilter filter = context.getBean(MetricsFilter.class);
CountDownLatch latch = new CountDownLatch(1);
MockMvc mvc = MockMvcBuilders
.standaloneSetup(new MetricFilterTestController(latch)).addFilter(filter)
.build();
String attributeName = MetricsFilter.class.getName() + ".StopWatch";
MvcResult result = mvc.perform(post("/create")).andExpect(status().isOk())
.andExpect(request().asyncStarted())
.andExpect(request().attribute(attributeName, is(notNullValue())))
.andReturn();
latch.countDown();
mvc.perform(asyncDispatch(result)).andExpect(status().isCreated())
.andExpect(request().attribute(attributeName, is(nullValue())));
verify(context.getBean(CounterService.class)).increment("status.201.create");
context.close();
}
@Test
public void correctlyRecordsMetricsForFailedDeferredResultResponse()
throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class);
MetricsFilter filter = context.getBean(MetricsFilter.class);
CountDownLatch latch = new CountDownLatch(1);
MockMvc mvc = MockMvcBuilders
.standaloneSetup(new MetricFilterTestController(latch)).addFilter(filter)
.build();
String attributeName = MetricsFilter.class.getName() + ".StopWatch";
MvcResult result = mvc.perform(post("/createFailure")).andExpect(status().isOk())
.andExpect(request().asyncStarted())
.andExpect(request().attribute(attributeName, is(notNullValue())))
.andReturn();
latch.countDown();
try {
mvc.perform(asyncDispatch(result));
fail();
}
catch (Exception ex) {
assertThat(result.getRequest().getAttribute(attributeName)).isNull();
verify(context.getBean(CounterService.class))
.increment("status.500.createFailure");
}
finally {
context.close();
}
}
@Test
public void records5xxxHttpInteractionsAsSingleMetric() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Config.class, MetricFilterAutoConfiguration.class,
ServiceUnavailableFilter.class);
MetricsFilter filter = context.getBean(MetricsFilter.class);
MockMvc mvc = MockMvcBuilders.standaloneSetup(new MetricFilterTestController())
.addFilter(filter)
.addFilter(context.getBean(ServiceUnavailableFilter.class)).build();
mvc.perform(get("/unknownPath/1")).andExpect(status().isServiceUnavailable());
mvc.perform(get("/unknownPath/2")).andExpect(status().isServiceUnavailable());
verify(context.getBean(CounterService.class), times(2))
.increment("status.503.unmapped");
verify(context.getBean(GaugeService.class), times(2))
.submit(eq("response.unmapped"), anyDouble());
context.close();
}
@Test
public void additionallyRecordsMetricsWithHttpMethodNameIfConfigured()
throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Config.class, MetricFilterAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(context,
"endpoints.metrics.filter.gauge-submissions=merged,per-http-method",
"endpoints.metrics.filter.counter-submissions=merged,per-http-method");
context.refresh();
Filter filter = context.getBean(Filter.class);
final MockHttpServletRequest request = new MockHttpServletRequest("PUT",
"/test/path");
final MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
response.setStatus(200);
return null;
}
}).given(chain).doFilter(request, response);
filter.doFilter(request, response, chain);
verify(context.getBean(GaugeService.class)).submit(eq("response.test.path"),
anyDouble());
verify(context.getBean(GaugeService.class)).submit(eq("response.PUT.test.path"),
anyDouble());
verify(context.getBean(CounterService.class))
.increment(eq("status.200.test.path"));
verify(context.getBean(CounterService.class))
.increment(eq("status.PUT.200.test.path"));
context.close();
}
@Test
public void doesNotRecordRolledUpMetricsIfConfigured() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Config.class, MetricFilterAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(context,
"endpoints.metrics.filter.gauge-submissions=",
"endpoints.metrics.filter.counter-submissions=");
context.refresh();
Filter filter = context.getBean(Filter.class);
final MockHttpServletRequest request = new MockHttpServletRequest("PUT",
"/test/path");
final MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
response.setStatus(200);
return null;
}
}).given(chain).doFilter(request, response);
filter.doFilter(request, response, chain);
verify(context.getBean(GaugeService.class), never()).submit(anyString(),
anyDouble());
verify(context.getBean(CounterService.class), never()).increment(anyString());
context.close();
}
@Test
public void whenExceptionIsThrownResponseStatusIsUsedWhenResponseHasBeenCommitted()
throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Config.class, MetricFilterAutoConfiguration.class);
context.refresh();
Filter filter = context.getBean(Filter.class);
final MockHttpServletRequest request = new MockHttpServletRequest("GET",
"/test/path");
final MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
response.setStatus(200);
response.setCommitted(true);
throw new IOException();
}
}).given(chain).doFilter(request, response);
try {
filter.doFilter(request, response, chain);
fail();
}
catch (IOException ex) {
// Continue
}
verify(context.getBean(CounterService.class))
.increment(eq("status.200.test.path"));
context.close();
}
@Configuration
public static class Config {
@Bean
public CounterService counterService() {
return mock(CounterService.class);
}
@Bean
public GaugeService gaugeService() {
return mock(GaugeService.class);
}
}
@RestController
class MetricFilterTestController {
private final CountDownLatch latch;
MetricFilterTestController() {
this(null);
}
MetricFilterTestController(CountDownLatch latch) {
this.latch = latch;
}
@RequestMapping("templateVarTest/{someVariable}")
public String testTemplateVariableResolution(@PathVariable String someVariable) {
return someVariable;
}
@RequestMapping("wildcardMapping/*")
public String testWildcardMapping() {
return "wildcard";
}
@RequestMapping("doubleWildcardMapping/**/baz")
public String testDoubleWildcardMapping() {
return "doubleWildcard";
}
@RequestMapping("templateVarRegexTest/{someVariable:[a-z]+}")
public String testTemplateVariableRegexResolution(
@PathVariable String someVariable) {
return someVariable;
}
@RequestMapping("knownPath/{someVariable}")
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseBody
public String testKnownPathWith404Response(@PathVariable String someVariable) {
return someVariable;
}
@ResponseBody
@RequestMapping("unhandledException")
public String testException() {
throw new RuntimeException();
}
@RequestMapping("create")
public DeferredResult<ResponseEntity<String>> create() {
final DeferredResult<ResponseEntity<String>> result = new DeferredResult<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
MetricFilterTestController.this.latch.await();
result.setResult(
new ResponseEntity<>("Done", HttpStatus.CREATED));
}
catch (InterruptedException ex) {
}
}
}).start();
return result;
}
@RequestMapping("createFailure")
public DeferredResult<ResponseEntity<String>> createFailure() {
final DeferredResult<ResponseEntity<String>> result = new DeferredResult<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
MetricFilterTestController.this.latch.await();
result.setErrorResult(new Exception("It failed"));
}
catch (InterruptedException ex) {
}
}
}).start();
return result;
}
}
@Component
@Order(0)
public static class RedirectFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// send redirect before filter chain is executed, like Spring Security sending
// us back to a login page
response.sendRedirect("http://example.com");
}
}
@Component
@Order(0)
public static class ServiceUnavailableFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value());
}
}
}