Skip to content

Commit 4bc32e6

Browse files
committed
Use a HandlerInterceptor for timing long tasks
Closes gh-15204
1 parent 958c386 commit 4bc32e6

File tree

6 files changed

+433
-137
lines changed

6 files changed

+433
-137
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
2828
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
2929
import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
30+
import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor;
3031
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter;
3132
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
3233
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -41,8 +42,9 @@
4142
import org.springframework.context.annotation.Configuration;
4243
import org.springframework.core.Ordered;
4344
import org.springframework.core.annotation.Order;
44-
import org.springframework.web.context.WebApplicationContext;
4545
import org.springframework.web.servlet.DispatcherServlet;
46+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
47+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4648

4749
/**
4850
* {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring Web
@@ -75,11 +77,10 @@ public DefaultWebMvcTagsProvider webMvcTagsProvider() {
7577

7678
@Bean
7779
public FilterRegistrationBean<WebMvcMetricsFilter> webMvcMetricsFilter(
78-
MeterRegistry registry, WebMvcTagsProvider tagsProvider,
79-
WebApplicationContext context) {
80+
MeterRegistry registry, WebMvcTagsProvider tagsProvider) {
8081
Server serverProperties = this.properties.getWeb().getServer();
81-
WebMvcMetricsFilter filter = new WebMvcMetricsFilter(context, registry,
82-
tagsProvider, serverProperties.getRequestsMetricName(),
82+
WebMvcMetricsFilter filter = new WebMvcMetricsFilter(registry, tagsProvider,
83+
serverProperties.getRequestsMetricName(),
8384
serverProperties.isAutoTimeRequests());
8485
FilterRegistrationBean<WebMvcMetricsFilter> registration = new FilterRegistrationBean<>(
8586
filter);
@@ -98,4 +99,30 @@ public MeterFilter metricsHttpServerUriTagFilter() {
9899
this.properties.getWeb().getServer().getMaxUriTags(), filter);
99100
}
100101

102+
@Bean
103+
public MetricsWebMvcConfigurer metricsWebMvcConfigurer(MeterRegistry meterRegistry,
104+
WebMvcTagsProvider tagsProvider) {
105+
return new MetricsWebMvcConfigurer(meterRegistry, tagsProvider);
106+
}
107+
108+
static class MetricsWebMvcConfigurer implements WebMvcConfigurer {
109+
110+
private final MeterRegistry meterRegistry;
111+
112+
private final WebMvcTagsProvider tagsProvider;
113+
114+
MetricsWebMvcConfigurer(MeterRegistry meterRegistry,
115+
WebMvcTagsProvider tagsProvider) {
116+
this.meterRegistry = meterRegistry;
117+
this.tagsProvider = tagsProvider;
118+
}
119+
120+
@Override
121+
public void addInterceptors(InterceptorRegistry registry) {
122+
registry.addInterceptor(new LongTaskTimingHandlerInterceptor(
123+
this.meterRegistry, this.tagsProvider));
124+
}
125+
126+
}
127+
101128
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
3434
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
3535
import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
36+
import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor;
3637
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter;
3738
import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
3839
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -47,6 +48,7 @@
4748
import org.springframework.test.web.servlet.MockMvc;
4849
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
4950
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
51+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
5052

5153
import static org.assertj.core.api.Assertions.assertThat;
5254
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -140,6 +142,22 @@ public void shouldNotDenyNorLogIfMaxUrisIsNotReached() {
140142
});
141143
}
142144

145+
@Test
146+
@SuppressWarnings("rawtypes")
147+
public void longTaskTimingInterceptorIsRegistered() {
148+
this.contextRunner
149+
.withUserConfiguration(TestController.class,
150+
MeterRegistryConfiguration.class)
151+
.withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class,
152+
WebMvcAutoConfiguration.class))
153+
.run((context) -> {
154+
assertThat(context.getBean(RequestMappingHandlerMapping.class))
155+
.extracting("interceptors").element(0).asList()
156+
.extracting((item) -> (Class) item.getClass())
157+
.contains(LongTaskTimingHandlerInterceptor.class);
158+
});
159+
}
160+
143161
private MeterRegistry getInitializedMeterRegistry(
144162
AssertableWebApplicationContext context) throws Exception {
145163
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.metrics.web.servlet;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.util.ArrayList;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Set;
25+
26+
import javax.servlet.http.HttpServletRequest;
27+
import javax.servlet.http.HttpServletResponse;
28+
29+
import io.micrometer.core.annotation.Timed;
30+
import io.micrometer.core.instrument.LongTaskTimer;
31+
import io.micrometer.core.instrument.MeterRegistry;
32+
import io.micrometer.core.instrument.Tag;
33+
34+
import org.springframework.core.annotation.AnnotationUtils;
35+
import org.springframework.web.method.HandlerMethod;
36+
import org.springframework.web.servlet.HandlerInterceptor;
37+
38+
/**
39+
* A {@link HandlerInterceptor} that supports Micrometer's long task timers configured on
40+
* a handler using {@link Timed} with {@link Timed#longTask()} set to {@code true}.
41+
*
42+
* @author Andy Wilkinson
43+
* @since 2.0.7
44+
*/
45+
public class LongTaskTimingHandlerInterceptor implements HandlerInterceptor {
46+
47+
private final MeterRegistry registry;
48+
49+
private final WebMvcTagsProvider tagsProvider;
50+
51+
/**
52+
* Creates a new {@ode LongTaskTimingHandlerInterceptor} that will create
53+
* {@link LongTaskTimer LongTaskTimers} using the given registry. Timers will be
54+
* tagged using the given {@code tagsProvider}.
55+
* @param registry the registry
56+
* @param tagsProvider the tags provider
57+
*/
58+
public LongTaskTimingHandlerInterceptor(MeterRegistry registry,
59+
WebMvcTagsProvider tagsProvider) {
60+
this.registry = registry;
61+
this.tagsProvider = tagsProvider;
62+
}
63+
64+
@Override
65+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
66+
Object handler) throws Exception {
67+
LongTaskTimingContext timingContext = LongTaskTimingContext.get(request);
68+
if (timingContext == null) {
69+
startAndAttachTimingContext(request, handler);
70+
}
71+
return true;
72+
}
73+
74+
@Override
75+
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
76+
Object handler, Exception ex) throws Exception {
77+
if (!request.isAsyncStarted()) {
78+
stopLongTaskTimers(LongTaskTimingContext.get(request));
79+
}
80+
}
81+
82+
private void startAndAttachTimingContext(HttpServletRequest request, Object handler) {
83+
Set<Timed> annotations = getTimedAnnotations(handler);
84+
Collection<LongTaskTimer.Sample> longTaskTimerSamples = getLongTaskTimerSamples(
85+
request, handler, annotations);
86+
LongTaskTimingContext timingContext = new LongTaskTimingContext(
87+
longTaskTimerSamples);
88+
timingContext.attachTo(request);
89+
}
90+
91+
private Collection<LongTaskTimer.Sample> getLongTaskTimerSamples(
92+
HttpServletRequest request, Object handler, Set<Timed> annotations) {
93+
List<LongTaskTimer.Sample> samples = new ArrayList<>();
94+
annotations.stream().filter(Timed::longTask).forEach((annotation) -> {
95+
Iterable<Tag> tags = this.tagsProvider.getLongRequestTags(request, handler);
96+
LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags);
97+
LongTaskTimer timer = builder.register(this.registry);
98+
samples.add(timer.start());
99+
});
100+
return samples;
101+
}
102+
103+
private Set<Timed> getTimedAnnotations(Object handler) {
104+
if (!(handler instanceof HandlerMethod)) {
105+
return Collections.emptySet();
106+
}
107+
return getTimedAnnotations((HandlerMethod) handler);
108+
}
109+
110+
private Set<Timed> getTimedAnnotations(HandlerMethod handler) {
111+
Set<Timed> timed = findTimedAnnotations(handler.getMethod());
112+
if (timed.isEmpty()) {
113+
return findTimedAnnotations(handler.getBeanType());
114+
}
115+
return timed;
116+
}
117+
118+
private Set<Timed> findTimedAnnotations(AnnotatedElement element) {
119+
return AnnotationUtils.getDeclaredRepeatableAnnotations(element, Timed.class);
120+
}
121+
122+
private void stopLongTaskTimers(LongTaskTimingContext timingContext) {
123+
for (LongTaskTimer.Sample sample : timingContext.getLongTaskTimerSamples()) {
124+
sample.stop();
125+
}
126+
}
127+
128+
/**
129+
* Context object attached to a request to retain information across the multiple
130+
* interceptor calls that happen with async requests.
131+
*/
132+
static class LongTaskTimingContext {
133+
134+
private static final String ATTRIBUTE = LongTaskTimingContext.class.getName();
135+
136+
private final Collection<LongTaskTimer.Sample> longTaskTimerSamples;
137+
138+
LongTaskTimingContext(Collection<LongTaskTimer.Sample> longTaskTimerSamples) {
139+
this.longTaskTimerSamples = longTaskTimerSamples;
140+
}
141+
142+
Collection<LongTaskTimer.Sample> getLongTaskTimerSamples() {
143+
return this.longTaskTimerSamples;
144+
}
145+
146+
void attachTo(HttpServletRequest request) {
147+
request.setAttribute(ATTRIBUTE, this);
148+
}
149+
150+
static LongTaskTimingContext get(HttpServletRequest request) {
151+
return (LongTaskTimingContext) request.getAttribute(ATTRIBUTE);
152+
}
153+
154+
}
155+
156+
}

0 commit comments

Comments
 (0)