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:
@@ -21,6 +21,7 @@ import java.io.StringWriter;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
@@ -61,7 +62,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
*/
|
||||
public class DefaultErrorAttributes implements ErrorAttributes {
|
||||
|
||||
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
|
||||
private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
|
||||
@@ -147,13 +148,15 @@ public class DefaultErrorAttributes implements ErrorAttributes {
|
||||
|
||||
@Override
|
||||
public Throwable getError(ServerRequest request) {
|
||||
return (Throwable) request.attribute(ERROR_ATTRIBUTE)
|
||||
Optional<Object> error = request.attribute(ERROR_INTERNAL_ATTRIBUTE);
|
||||
error.ifPresent((value) -> request.attributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, value));
|
||||
return (Throwable) error
|
||||
.orElseThrow(() -> new IllegalStateException("Missing exception attribute in ServerWebExchange"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeErrorInformation(Throwable error, ServerWebExchange exchange) {
|
||||
exchange.getAttributes().putIfAbsent(ERROR_ATTRIBUTE, error);
|
||||
exchange.getAttributes().putIfAbsent(ERROR_INTERNAL_ATTRIBUTE, error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
*/
|
||||
public interface ErrorAttributes {
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerRequest#attribute(String)} Request attribute} holding the
|
||||
* error resolved by the {@code ErrorAttributes} implementation.
|
||||
* @since 2.5.0
|
||||
*/
|
||||
String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error";
|
||||
|
||||
/**
|
||||
* Return a {@link Map} of the error attributes. The map can be used as the model of
|
||||
* an error page, or returned as a {@link ServerResponse} body.
|
||||
|
||||
@@ -68,7 +68,7 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
|
||||
|
||||
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
|
||||
private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
@@ -83,7 +83,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
|
||||
}
|
||||
|
||||
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
|
||||
request.setAttribute(ERROR_ATTRIBUTE, ex);
|
||||
request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -216,8 +216,14 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
|
||||
|
||||
@Override
|
||||
public Throwable getError(WebRequest webRequest) {
|
||||
Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
|
||||
return (exception != null) ? exception : getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
|
||||
Throwable exception = getAttribute(webRequest, ERROR_INTERNAL_ATTRIBUTE);
|
||||
if (exception == null) {
|
||||
exception = getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
|
||||
}
|
||||
// store the exception in a well-known attribute to make it available to metrics
|
||||
// instrumentation.
|
||||
webRequest.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, exception, WebRequest.SCOPE_REQUEST);
|
||||
return exception;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -34,6 +34,14 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
*/
|
||||
public interface ErrorAttributes {
|
||||
|
||||
/**
|
||||
* Name of the {@link javax.servlet.http.HttpServletRequest#getAttribute(String)
|
||||
* Request attribute} holding the error resolved by the {@code ErrorAttributes}
|
||||
* implementation.
|
||||
* @since 2.5.0
|
||||
*/
|
||||
String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error";
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} of the error attributes. The map can be used as the model of
|
||||
* an error page {@link ModelAndView}, or returned as a
|
||||
|
||||
@@ -160,6 +160,7 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
||||
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error);
|
||||
assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error);
|
||||
assertThat(attributes.get("exception")).isEqualTo(RuntimeException.class.getName());
|
||||
assertThat(attributes.get("message")).isEqualTo("Test");
|
||||
}
|
||||
@@ -177,6 +178,7 @@ class DefaultErrorAttributesTests {
|
||||
assertThat(attributes.get("message")).isEqualTo("invalid request");
|
||||
assertThat(attributes.get("exception")).isEqualTo(RuntimeException.class.getName());
|
||||
assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error);
|
||||
assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -192,6 +194,7 @@ class DefaultErrorAttributesTests {
|
||||
assertThat(attributes.get("message")).isEqualTo("could not process request");
|
||||
assertThat(attributes.get("exception")).isEqualTo(ResponseStatusException.class.getName());
|
||||
assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error);
|
||||
assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -89,6 +89,8 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST))
|
||||
.isSameAs(ex);
|
||||
assertThat(modelAndView).isNull();
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes.get("message")).isEqualTo("Test");
|
||||
@@ -101,6 +103,8 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST))
|
||||
.isSameAs(ex);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes.get("message")).isEqualTo("Test");
|
||||
}
|
||||
@@ -112,6 +116,8 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST))
|
||||
.isSameAs(ex);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).doesNotContainKey("message");
|
||||
}
|
||||
@@ -161,6 +167,8 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(wrapped);
|
||||
assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST))
|
||||
.isSameAs(wrapped);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes.get("message")).isEqualTo("Test");
|
||||
}
|
||||
@@ -172,6 +180,8 @@ class DefaultErrorAttributesTests {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(error);
|
||||
assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST))
|
||||
.isSameAs(error);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes.get("message")).isEqualTo("Test error");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user