Skip to content

Commit 78aa523

Browse files
committed
Merge pull request #23112 from anshlykov
* pr/23112: Polish 'Support @timed annotation for WebFlux' Support @timed annotation for WebFlux Closes gh-23112
2 parents 816bd14 + 15fe418 commit 78aa523

File tree

5 files changed

+256
-35
lines changed

5 files changed

+256
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2012-2021 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+
* https://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.method;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import io.micrometer.core.annotation.Timed;
25+
26+
import org.springframework.core.annotation.MergedAnnotationCollectors;
27+
import org.springframework.core.annotation.MergedAnnotations;
28+
import org.springframework.util.ConcurrentReferenceHashMap;
29+
import org.springframework.web.method.HandlerMethod;
30+
31+
/**
32+
* Utility used to obtain {@link Timed @Timed} annotations from a {@link HandlerMethod}.
33+
*
34+
* @author Phillip Webb
35+
* @since 2.5.0
36+
*/
37+
public final class HandlerMethodTimedAnnotations {
38+
39+
private static Map<AnnotatedElement, Set<Timed>> cache = new ConcurrentReferenceHashMap<>();
40+
41+
private HandlerMethodTimedAnnotations() {
42+
}
43+
44+
public static Set<Timed> get(HandlerMethod handler) {
45+
Set<Timed> methodAnnotations = findTimedAnnotations(handler.getMethod());
46+
if (!methodAnnotations.isEmpty()) {
47+
return methodAnnotations;
48+
}
49+
return findTimedAnnotations(handler.getBeanType());
50+
}
51+
52+
private static Set<Timed> findTimedAnnotations(AnnotatedElement element) {
53+
Set<Timed> result = cache.get(element);
54+
if (result != null) {
55+
return result;
56+
}
57+
MergedAnnotations annotations = MergedAnnotations.from(element);
58+
result = (!annotations.isPresent(Timed.class)) ? Collections.emptySet()
59+
: annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
60+
cache.put(element, result);
61+
return result;
62+
}
63+
64+
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@
1616

1717
package org.springframework.boot.actuate.metrics.web.reactive.server;
1818

19+
import java.util.Collections;
20+
import java.util.Set;
1921
import java.util.concurrent.TimeUnit;
2022

23+
import io.micrometer.core.annotation.Timed;
2124
import io.micrometer.core.instrument.MeterRegistry;
2225
import io.micrometer.core.instrument.Tag;
26+
import io.micrometer.core.instrument.Timer;
27+
import io.micrometer.core.instrument.Timer.Builder;
2328
import org.reactivestreams.Publisher;
2429
import reactor.core.publisher.Mono;
2530

2631
import org.springframework.boot.actuate.metrics.AutoTimer;
32+
import org.springframework.boot.actuate.metrics.web.method.HandlerMethodTimedAnnotations;
2733
import org.springframework.boot.web.reactive.error.ErrorAttributes;
2834
import org.springframework.core.Ordered;
2935
import org.springframework.core.annotation.Order;
3036
import org.springframework.http.server.reactive.ServerHttpResponse;
37+
import org.springframework.web.method.HandlerMethod;
38+
import org.springframework.web.reactive.HandlerMapping;
3139
import org.springframework.web.server.ServerWebExchange;
3240
import org.springframework.web.server.WebFilter;
3341
import org.springframework.web.server.WebFilterChain;
@@ -68,9 +76,6 @@ public MetricsWebFilter(MeterRegistry registry, WebFluxTagsProvider tagsProvider
6876

6977
@Override
7078
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
71-
if (!this.autoTimer.isEnabled()) {
72-
return chain.filter(exchange);
73-
}
7479
return chain.filter(exchange).transformDeferred((call) -> filter(exchange, call));
7580
}
7681

@@ -94,12 +99,24 @@ private void onTerminalSignal(ServerWebExchange exchange, Throwable cause, long
9499
}
95100

96101
private void record(ServerWebExchange exchange, Throwable cause, long start) {
97-
if (cause == null) {
98-
cause = exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
99-
}
102+
cause = (cause != null) ? cause : exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
103+
Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
104+
Set<Timed> annotations = (handler instanceof HandlerMethod)
105+
? HandlerMethodTimedAnnotations.get((HandlerMethod) handler) : Collections.emptySet();
100106
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
101-
this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(System.nanoTime() - start,
102-
TimeUnit.NANOSECONDS);
107+
long duration = System.nanoTime() - start;
108+
if (annotations.isEmpty()) {
109+
if (this.autoTimer.isEnabled()) {
110+
Builder builder = this.autoTimer.builder(this.metricName);
111+
builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS);
112+
}
113+
}
114+
else {
115+
for (Timed annotation : annotations) {
116+
Builder builder = Timer.builder(annotation, this.metricName);
117+
builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS);
118+
}
119+
}
103120
}
104121

105122
}

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

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.actuate.metrics.web.servlet;
1818

1919
import java.io.IOException;
20-
import java.lang.reflect.AnnotatedElement;
2120
import java.util.Collections;
2221
import java.util.Set;
2322

@@ -33,9 +32,8 @@
3332
import io.micrometer.core.instrument.Timer.Sample;
3433

3534
import org.springframework.boot.actuate.metrics.AutoTimer;
35+
import org.springframework.boot.actuate.metrics.web.method.HandlerMethodTimedAnnotations;
3636
import org.springframework.boot.web.servlet.error.ErrorAttributes;
37-
import org.springframework.core.annotation.MergedAnnotationCollectors;
38-
import org.springframework.core.annotation.MergedAnnotations;
3937
import org.springframework.http.HttpStatus;
4038
import org.springframework.web.filter.OncePerRequestFilter;
4139
import org.springframework.web.method.HandlerMethod;
@@ -130,7 +128,8 @@ private Throwable fetchException(HttpServletRequest request) {
130128
private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,
131129
Throwable exception) {
132130
Object handler = getHandler(request);
133-
Set<Timed> annotations = getTimedAnnotations(handler);
131+
Set<Timed> annotations = (handler instanceof HandlerMethod)
132+
? HandlerMethodTimedAnnotations.get((HandlerMethod) handler) : Collections.emptySet();
134133
Timer.Sample timerSample = timingContext.getTimerSample();
135134
if (annotations.isEmpty()) {
136135
if (this.autoTimer.isEnabled()) {
@@ -150,29 +149,6 @@ private Object getHandler(HttpServletRequest request) {
150149
return request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
151150
}
152151

153-
private Set<Timed> getTimedAnnotations(Object handler) {
154-
if (!(handler instanceof HandlerMethod)) {
155-
return Collections.emptySet();
156-
}
157-
return getTimedAnnotations((HandlerMethod) handler);
158-
}
159-
160-
private Set<Timed> getTimedAnnotations(HandlerMethod handler) {
161-
Set<Timed> methodAnnotations = findTimedAnnotations(handler.getMethod());
162-
if (!methodAnnotations.isEmpty()) {
163-
return methodAnnotations;
164-
}
165-
return findTimedAnnotations(handler.getBeanType());
166-
}
167-
168-
private Set<Timed> findTimedAnnotations(AnnotatedElement element) {
169-
MergedAnnotations annotations = MergedAnnotations.from(element);
170-
if (!annotations.isPresent(Timed.class)) {
171-
return Collections.emptySet();
172-
}
173-
return annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
174-
}
175-
176152
private Timer getTimer(Builder builder, Object handler, HttpServletRequest request, HttpServletResponse response,
177153
Throwable exception) {
178154
return builder.tags(this.tagsProvider.getTags(request, response, handler, exception)).register(this.registry);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2012-2021 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+
* https://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.method;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.Set;
21+
22+
import io.micrometer.core.annotation.Timed;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.util.ReflectionUtils;
26+
import org.springframework.web.method.HandlerMethod;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link HandlerMethodTimedAnnotations}.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
class HandlerMethodTimedAnnotationsTests {
36+
37+
@Test
38+
void getWhenNoneReturnsEmptySet() {
39+
Object bean = new None();
40+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
41+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
42+
assertThat(annotations).isEmpty();
43+
}
44+
45+
@Test
46+
void getWhenOnMethodReturnsMethodAnnotations() {
47+
Object bean = new OnMethod();
48+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
49+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
50+
assertThat(annotations).extracting(Timed::value).containsOnly("y", "z");
51+
}
52+
53+
@Test
54+
void getWhenNonOnMethodReturnsBeanAnnotations() {
55+
Object bean = new OnBean();
56+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
57+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
58+
assertThat(annotations).extracting(Timed::value).containsOnly("y", "z");
59+
}
60+
61+
static class None {
62+
63+
void handle() {
64+
}
65+
66+
}
67+
68+
@Timed("x")
69+
static class OnMethod {
70+
71+
@Timed("y")
72+
@Timed("z")
73+
void handle() {
74+
}
75+
76+
}
77+
78+
@Timed("y")
79+
@Timed("z")
80+
static class OnBean {
81+
82+
void handle() {
83+
}
84+
85+
}
86+
87+
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.EOFException;
2020
import java.time.Duration;
2121

22+
import io.micrometer.core.annotation.Timed;
2223
import io.micrometer.core.instrument.MockClock;
2324
import io.micrometer.core.instrument.simple.SimpleConfig;
2425
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
@@ -31,6 +32,8 @@
3132
import org.springframework.boot.web.reactive.error.ErrorAttributes;
3233
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
3334
import org.springframework.mock.web.server.MockServerWebExchange;
35+
import org.springframework.util.ReflectionUtils;
36+
import org.springframework.web.method.HandlerMethod;
3437
import org.springframework.web.reactive.HandlerMapping;
3538
import org.springframework.web.util.pattern.PathPatternParser;
3639

@@ -46,6 +49,8 @@ class MetricsWebFilterTests {
4649

4750
private static final String REQUEST_METRICS_NAME = "http.server.requests";
4851

52+
private static final String REQUEST_METRICS_NAME_PERCENTILE = REQUEST_METRICS_NAME + ".percentile";
53+
4954
private SimpleMeterRegistry registry;
5055

5156
private MetricsWebFilter webFilter;
@@ -157,6 +162,59 @@ void disconnectedExceptionShouldProduceMetrics() {
157162
assertMetricsContainsTag("outcome", "UNKNOWN");
158163
}
159164

165+
@Test
166+
void filterAddsStandardTags() {
167+
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timed");
168+
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
169+
.block(Duration.ofSeconds(30));
170+
assertMetricsContainsTag("uri", "/projects/{project}");
171+
assertMetricsContainsTag("status", "200");
172+
}
173+
174+
@Test
175+
void filterAddsExtraTags() {
176+
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags");
177+
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
178+
.block(Duration.ofSeconds(30));
179+
assertMetricsContainsTag("uri", "/projects/{project}");
180+
assertMetricsContainsTag("status", "200");
181+
assertMetricsContainsTag("tag1", "value1");
182+
assertMetricsContainsTag("tag2", "value2");
183+
}
184+
185+
@Test
186+
void filterAddsExtraTagsAndException() {
187+
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags");
188+
this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error")))
189+
.onErrorResume((ex) -> {
190+
exchange.getResponse().setRawStatusCode(500);
191+
return exchange.getResponse().setComplete();
192+
}).block(Duration.ofSeconds(30));
193+
assertMetricsContainsTag("uri", "/projects/{project}");
194+
assertMetricsContainsTag("status", "500");
195+
assertMetricsContainsTag("exception", "IllegalStateException");
196+
assertMetricsContainsTag("tag1", "value1");
197+
assertMetricsContainsTag("tag2", "value2");
198+
}
199+
200+
@Test
201+
void filterAddsPercentileMeters() {
202+
MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedPercentiles");
203+
this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete())
204+
.block(Duration.ofSeconds(30));
205+
assertMetricsContainsTag("uri", "/projects/{project}");
206+
assertMetricsContainsTag("status", "200");
207+
assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.95").gauge().value()).isNotZero();
208+
assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.5").gauge().value()).isNotZero();
209+
}
210+
211+
private MockServerWebExchange createTimedHandlerMethodExchange(String methodName) {
212+
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
213+
exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE,
214+
new HandlerMethod(this, ReflectionUtils.findMethod(Handlers.class, methodName)));
215+
return exchange;
216+
}
217+
160218
private MockServerWebExchange createExchange(String path, String pathPattern) {
161219
PathPatternParser parser = new PathPatternParser();
162220
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build());
@@ -168,4 +226,23 @@ private void assertMetricsContainsTag(String tagKey, String tagValue) {
168226
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1);
169227
}
170228

229+
static class Handlers {
230+
231+
@Timed
232+
Mono<String> timed() {
233+
return Mono.just("test");
234+
}
235+
236+
@Timed(extraTags = { "tag1", "value1", "tag2", "value2" })
237+
Mono<String> timedExtraTags() {
238+
return Mono.just("test");
239+
}
240+
241+
@Timed(percentiles = { 0.5, 0.95 })
242+
Mono<String> timedPercentiles() {
243+
return Mono.just("test");
244+
}
245+
246+
}
247+
171248
}

0 commit comments

Comments
 (0)