Allow to manually tag request metrics with exceptions
Prior to this commit, some exceptions handled at the controller or handler function level would: * not bubble up to the Spring Boot error handling support * not be tagged as part of the request metrics This situation is inconsistent because in general, exceptions handled at the controller level can be considered as expected behavior. Also, depending on how the exception is handled, the request metrics might not be tagged with the exception. This will be reconsidered in gh-23795. This commit prepares a transition to the new situation. Developers can now opt-in and set the handled exception as a request attribute. This well-known attribute will be later read by the metrics support and used for tagging the request metrics with the exception provided. This mechanism is automatically used by the error handling support in Spring Boot. Closes gh-24028
This commit is contained in:
@@ -24,6 +24,7 @@ import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
@@ -93,6 +94,9 @@ public class MetricsWebFilter implements WebFilter {
|
||||
}
|
||||
|
||||
private void record(ServerWebExchange exchange, Throwable cause, long start) {
|
||||
if (cause == null) {
|
||||
cause = exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
|
||||
}
|
||||
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
|
||||
this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(System.nanoTime() - start,
|
||||
TimeUnit.NANOSECONDS);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 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.
|
||||
@@ -33,6 +33,7 @@ import io.micrometer.core.instrument.Timer.Builder;
|
||||
import io.micrometer.core.instrument.Timer.Sample;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.core.annotation.MergedAnnotationCollectors;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -96,7 +97,7 @@ public class WebMvcMetricsFilter extends OncePerRequestFilter {
|
||||
// If async was started by something further down the chain we wait
|
||||
// until the second filter invocation (but we'll be using the
|
||||
// TimingContext that was attached to the first)
|
||||
Throwable exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
|
||||
Throwable exception = fetchException(request);
|
||||
record(timingContext, request, response, exception);
|
||||
}
|
||||
}
|
||||
@@ -118,6 +119,14 @@ public class WebMvcMetricsFilter extends OncePerRequestFilter {
|
||||
return timingContext;
|
||||
}
|
||||
|
||||
private Throwable fetchException(HttpServletRequest request) {
|
||||
Throwable exception = (Throwable) request.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
|
||||
if (exception == null) {
|
||||
exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
|
||||
}
|
||||
return exception;
|
||||
}
|
||||
|
||||
private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,
|
||||
Throwable exception) {
|
||||
Object handler = getHandler(request);
|
||||
|
||||
@@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
@@ -94,6 +95,18 @@ class MetricsWebFilterTests {
|
||||
assertMetricsContainsTag("exception", anonymous.getClass().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistryForHandledExceptions() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
this.webFilter.filter(exchange, (serverWebExchange) -> {
|
||||
exchange.getAttributes().put(ErrorAttributes.ERROR_ATTRIBUTE, new IllegalStateException("test error"));
|
||||
return exchange.getResponse().setComplete();
|
||||
}).block(Duration.ofSeconds(30));
|
||||
assertMetricsContainsTag("uri", "/projects/{project}");
|
||||
assertMetricsContainsTag("status", "200");
|
||||
assertMetricsContainsTag("exception", "IllegalStateException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterAddsTagsToRegistryForExceptionsAndCommittedResponse() {
|
||||
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 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.
|
||||
@@ -57,6 +57,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -264,7 +265,8 @@ class WebMvcMetricsFilterTests {
|
||||
@Test
|
||||
void endpointThrowsError() throws Exception {
|
||||
this.mvc.perform(get("/api/c1/error/10")).andExpect(status().is4xxClientError());
|
||||
assertThat(this.registry.get("http.server.requests").tags("status", "422").timer().count()).isEqualTo(1L);
|
||||
assertThat(this.registry.get("http.server.requests").tags("status", "422", "exception", "IllegalStateException")
|
||||
.timer().count()).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -491,6 +493,8 @@ class WebMvcMetricsFilterTests {
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||
ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) {
|
||||
// this is done by ErrorAttributes implementations
|
||||
request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, e);
|
||||
return new ModelAndView("myerror");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user