6 changed files with 433 additions and 137 deletions
@ -0,0 +1,156 @@
@@ -0,0 +1,156 @@
|
||||
/* |
||||
* Copyright 2012-2018 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.metrics.web.servlet; |
||||
|
||||
import java.lang.reflect.AnnotatedElement; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletResponse; |
||||
|
||||
import io.micrometer.core.annotation.Timed; |
||||
import io.micrometer.core.instrument.LongTaskTimer; |
||||
import io.micrometer.core.instrument.MeterRegistry; |
||||
import io.micrometer.core.instrument.Tag; |
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils; |
||||
import org.springframework.web.method.HandlerMethod; |
||||
import org.springframework.web.servlet.HandlerInterceptor; |
||||
|
||||
/** |
||||
* A {@link HandlerInterceptor} that supports Micrometer's long task timers configured on |
||||
* a handler using {@link Timed} with {@link Timed#longTask()} set to {@code true}. |
||||
* |
||||
* @author Andy Wilkinson |
||||
* @since 2.0.7 |
||||
*/ |
||||
public class LongTaskTimingHandlerInterceptor implements HandlerInterceptor { |
||||
|
||||
private final MeterRegistry registry; |
||||
|
||||
private final WebMvcTagsProvider tagsProvider; |
||||
|
||||
/** |
||||
* Creates a new {@ode LongTaskTimingHandlerInterceptor} that will create |
||||
* {@link LongTaskTimer LongTaskTimers} using the given registry. Timers will be |
||||
* tagged using the given {@code tagsProvider}. |
||||
* @param registry the registry |
||||
* @param tagsProvider the tags provider |
||||
*/ |
||||
public LongTaskTimingHandlerInterceptor(MeterRegistry registry, |
||||
WebMvcTagsProvider tagsProvider) { |
||||
this.registry = registry; |
||||
this.tagsProvider = tagsProvider; |
||||
} |
||||
|
||||
@Override |
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, |
||||
Object handler) throws Exception { |
||||
LongTaskTimingContext timingContext = LongTaskTimingContext.get(request); |
||||
if (timingContext == null) { |
||||
startAndAttachTimingContext(request, handler); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, |
||||
Object handler, Exception ex) throws Exception { |
||||
if (!request.isAsyncStarted()) { |
||||
stopLongTaskTimers(LongTaskTimingContext.get(request)); |
||||
} |
||||
} |
||||
|
||||
private void startAndAttachTimingContext(HttpServletRequest request, Object handler) { |
||||
Set<Timed> annotations = getTimedAnnotations(handler); |
||||
Collection<LongTaskTimer.Sample> longTaskTimerSamples = getLongTaskTimerSamples( |
||||
request, handler, annotations); |
||||
LongTaskTimingContext timingContext = new LongTaskTimingContext( |
||||
longTaskTimerSamples); |
||||
timingContext.attachTo(request); |
||||
} |
||||
|
||||
private Collection<LongTaskTimer.Sample> getLongTaskTimerSamples( |
||||
HttpServletRequest request, Object handler, Set<Timed> annotations) { |
||||
List<LongTaskTimer.Sample> samples = new ArrayList<>(); |
||||
annotations.stream().filter(Timed::longTask).forEach((annotation) -> { |
||||
Iterable<Tag> tags = this.tagsProvider.getLongRequestTags(request, handler); |
||||
LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags); |
||||
LongTaskTimer timer = builder.register(this.registry); |
||||
samples.add(timer.start()); |
||||
}); |
||||
return samples; |
||||
} |
||||
|
||||
private Set<Timed> getTimedAnnotations(Object handler) { |
||||
if (!(handler instanceof HandlerMethod)) { |
||||
return Collections.emptySet(); |
||||
} |
||||
return getTimedAnnotations((HandlerMethod) handler); |
||||
} |
||||
|
||||
private Set<Timed> getTimedAnnotations(HandlerMethod handler) { |
||||
Set<Timed> timed = findTimedAnnotations(handler.getMethod()); |
||||
if (timed.isEmpty()) { |
||||
return findTimedAnnotations(handler.getBeanType()); |
||||
} |
||||
return timed; |
||||
} |
||||
|
||||
private Set<Timed> findTimedAnnotations(AnnotatedElement element) { |
||||
return AnnotationUtils.getDeclaredRepeatableAnnotations(element, Timed.class); |
||||
} |
||||
|
||||
private void stopLongTaskTimers(LongTaskTimingContext timingContext) { |
||||
for (LongTaskTimer.Sample sample : timingContext.getLongTaskTimerSamples()) { |
||||
sample.stop(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Context object attached to a request to retain information across the multiple |
||||
* interceptor calls that happen with async requests. |
||||
*/ |
||||
static class LongTaskTimingContext { |
||||
|
||||
private static final String ATTRIBUTE = LongTaskTimingContext.class.getName(); |
||||
|
||||
private final Collection<LongTaskTimer.Sample> longTaskTimerSamples; |
||||
|
||||
LongTaskTimingContext(Collection<LongTaskTimer.Sample> longTaskTimerSamples) { |
||||
this.longTaskTimerSamples = longTaskTimerSamples; |
||||
} |
||||
|
||||
Collection<LongTaskTimer.Sample> getLongTaskTimerSamples() { |
||||
return this.longTaskTimerSamples; |
||||
} |
||||
|
||||
void attachTo(HttpServletRequest request) { |
||||
request.setAttribute(ATTRIBUTE, this); |
||||
} |
||||
|
||||
static LongTaskTimingContext get(HttpServletRequest request) { |
||||
return (LongTaskTimingContext) request.getAttribute(ATTRIBUTE); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
|
||||
/* |
||||
* Copyright 2012-2018 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.metrics.web.servlet; |
||||
|
||||
import java.util.concurrent.Callable; |
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.CyclicBarrier; |
||||
import java.util.concurrent.atomic.AtomicReference; |
||||
|
||||
import io.micrometer.core.annotation.Timed; |
||||
import io.micrometer.core.instrument.Clock; |
||||
import io.micrometer.core.instrument.MeterRegistry; |
||||
import io.micrometer.core.instrument.MockClock; |
||||
import io.micrometer.core.instrument.simple.SimpleConfig; |
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
import org.springframework.test.context.web.WebAppConfiguration; |
||||
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.GetMapping; |
||||
import org.springframework.web.bind.annotation.PathVariable; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.context.WebApplicationContext; |
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc; |
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
||||
import org.springframework.web.util.NestedServletException; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.fail; |
||||
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; |
||||
|
||||
/** |
||||
* Tests for {@link LongTaskTimingHandlerInterceptor}. |
||||
* |
||||
* @author Andy Wilkinson |
||||
*/ |
||||
@RunWith(SpringRunner.class) |
||||
@WebAppConfiguration |
||||
public class LongTaskTimingHandlerInterceptorTests { |
||||
|
||||
@Autowired |
||||
private SimpleMeterRegistry registry; |
||||
|
||||
@Autowired |
||||
private WebApplicationContext context; |
||||
|
||||
@Autowired |
||||
private CyclicBarrier callableBarrier; |
||||
|
||||
private MockMvc mvc; |
||||
|
||||
@Before |
||||
public void setUpMockMvc() { |
||||
this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); |
||||
} |
||||
|
||||
@Test |
||||
public void asyncRequestThatThrowsUncheckedException() throws Exception { |
||||
MvcResult result = this.mvc.perform(get("/api/c1/completableFutureException")) |
||||
.andExpect(request().asyncStarted()).andReturn(); |
||||
assertThat(this.registry.get("my.long.request.exception").longTaskTimer() |
||||
.activeTasks()).isEqualTo(1); |
||||
assertThatExceptionOfType(NestedServletException.class) |
||||
.isThrownBy(() -> this.mvc.perform(asyncDispatch(result))) |
||||
.withRootCauseInstanceOf(RuntimeException.class); |
||||
assertThat(this.registry.get("my.long.request.exception").longTaskTimer() |
||||
.activeTasks()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
public void asyncCallableRequest() throws Exception { |
||||
AtomicReference<MvcResult> result = new AtomicReference<>(); |
||||
Thread backgroundRequest = new Thread(() -> { |
||||
try { |
||||
result.set(this.mvc.perform(get("/api/c1/callable/10")) |
||||
.andExpect(request().asyncStarted()).andReturn()); |
||||
} |
||||
catch (Exception ex) { |
||||
fail("Failed to execute async request", ex); |
||||
} |
||||
}); |
||||
backgroundRequest.start(); |
||||
this.callableBarrier.await(); |
||||
assertThat(this.registry.get("my.long.request").tags("region", "test") |
||||
.longTaskTimer().activeTasks()).isEqualTo(1); |
||||
this.callableBarrier.await(); |
||||
backgroundRequest.join(); |
||||
this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); |
||||
assertThat(this.registry.get("my.long.request").tags("region", "test") |
||||
.longTaskTimer().activeTasks()).isEqualTo(0); |
||||
} |
||||
|
||||
@Configuration |
||||
@EnableWebMvc |
||||
@Import(Controller1.class) |
||||
static class MetricsInterceptorConfiguration { |
||||
|
||||
@Bean |
||||
Clock micrometerClock() { |
||||
return new MockClock(); |
||||
} |
||||
|
||||
@Bean |
||||
SimpleMeterRegistry simple(Clock clock) { |
||||
return new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); |
||||
} |
||||
|
||||
@Bean |
||||
CyclicBarrier callableBarrier() { |
||||
return new CyclicBarrier(2); |
||||
} |
||||
|
||||
@Bean |
||||
WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry) { |
||||
return new WebMvcConfigurer() { |
||||
|
||||
@Override |
||||
public void addInterceptors(InterceptorRegistry registry) { |
||||
registry.addInterceptor(new LongTaskTimingHandlerInterceptor( |
||||
meterRegistry, new DefaultWebMvcTagsProvider())); |
||||
} |
||||
|
||||
}; |
||||
} |
||||
|
||||
} |
||||
|
||||
@RestController |
||||
@RequestMapping("/api/c1") |
||||
static class Controller1 { |
||||
|
||||
@Autowired |
||||
private CyclicBarrier callableBarrier; |
||||
|
||||
@Timed |
||||
@Timed(value = "my.long.request", extraTags = { "region", |
||||
"test" }, longTask = true) |
||||
@GetMapping("/callable/{id}") |
||||
public Callable<String> asyncCallable(@PathVariable Long id) throws Exception { |
||||
this.callableBarrier.await(); |
||||
return () -> { |
||||
try { |
||||
this.callableBarrier.await(); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new RuntimeException(ex); |
||||
} |
||||
return id.toString(); |
||||
}; |
||||
} |
||||
|
||||
@Timed |
||||
@Timed(value = "my.long.request.exception", longTask = true) |
||||
@GetMapping("/completableFutureException") |
||||
CompletableFuture<String> asyncCompletableFutureException() { |
||||
return CompletableFuture.supplyAsync(() -> { |
||||
throw new RuntimeException("boom"); |
||||
}); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue