package org.springframework.boot.actuate.metrics.web.servlet;
import java.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.util.Collections;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.Timer.Builder;
import io.micrometer.core.instrument.Timer.Sample;
import org.springframework.boot.actuate.metrics.AutoTimer;
import org.springframework.core.annotation.MergedAnnotationCollectors;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.NestedServletException;
public class WebMvcMetricsFilter extends OncePerRequestFilter {
private final MeterRegistry registry;
private final WebMvcTagsProvider tagsProvider;
private final String metricName;
private final AutoTimer autoTimer;
public WebMvcMetricsFilter(MeterRegistry registry, WebMvcTagsProvider tagsProvider, String metricName,
AutoTimer autoTimer) {
this.registry = registry;
this.tagsProvider = tagsProvider;
this.metricName = metricName;
this.autoTimer = autoTimer;
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
TimingContext timingContext = TimingContext.get(request);
if (timingContext == null) {
timingContext = startAndAttachTimingContext(request);
}
try {
filterChain.doFilter(request, response);
if (!request.isAsyncStarted()) {
Throwable exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
record(timingContext, request, response, exception);
}
}
catch (NestedServletException ex) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
record(timingContext, request, response, ex.getCause());
throw ex;
}
catch (ServletException | IOException | RuntimeException ex) {
record(timingContext, request, response, ex);
throw ex;
}
}
private TimingContext startAndAttachTimingContext(HttpServletRequest request) {
Timer.Sample timerSample = Timer.start(this.registry);
TimingContext timingContext = new TimingContext(timerSample);
timingContext.attachTo(request);
return timingContext;
}
private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,
Throwable exception) {
Object handler = getHandler(request);
Set<Timed> annotations = getTimedAnnotations(handler);
Timer.Sample timerSample = timingContext.getTimerSample();
if (annotations.isEmpty()) {
if (this.autoTimer.isEnabled()) {
Builder builder = this.autoTimer.builder(this.metricName);
timerSample.stop(getTimer(builder, handler, request, response, exception));
}
}
else {
for (Timed annotation : annotations) {
Builder builder = Timer.builder(annotation, this.metricName);
timerSample.stop(getTimer(builder, handler, request, response, exception));
}
}
}
private Object getHandler(HttpServletRequest request) {
return request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
}
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> methodAnnotations = findTimedAnnotations(handler.getMethod());
if (!methodAnnotations.isEmpty()) {
return methodAnnotations;
}
return findTimedAnnotations(handler.getBeanType());
}
private Set<Timed> findTimedAnnotations(AnnotatedElement element) {
MergedAnnotations annotations = MergedAnnotations.from(element);
if (!annotations.isPresent(Timed.class)) {
return Collections.emptySet();
}
return annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
}
private Timer getTimer(Builder builder, Object handler, HttpServletRequest request, HttpServletResponse response,
Throwable exception) {
return builder.tags(this.tagsProvider.getTags(request, response, handler, exception)).register(this.registry);
}
private static class TimingContext {
private static final String ATTRIBUTE = TimingContext.class.getName();
private final Timer.Sample timerSample;
TimingContext(Sample timerSample) {
this.timerSample = timerSample;
}
Timer.Sample getTimerSample() {
return this.timerSample;
}
void attachTo(HttpServletRequest request) {
request.setAttribute(ATTRIBUTE, this);
}
static TimingContext get(HttpServletRequest request) {
return (TimingContext) request.getAttribute(ATTRIBUTE);
}
}
}