Globally Logging all Spring (Boot) Exceptions

Featured image for sharing metadata for article

There are several ways to set up exception handling in a Spring project, so you can map a Java Exception to an HTTP response.

But regardless of how you set it up, it's likely that operationally, you'd want to understand when users are going through common error flows. This may lead you to a class like this, with various exception handlers:

import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import me.jvt.hacking.exception.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {

  private static final Logger LOGGER = LoggerFactory.getLogger(MicropubExceptionHandler.class);

  @ExceptionHandler(ConstraintViolationException.class)
  public final ResponseEntity<ErrorResponse> constraintViolationException(
      ConstraintViolationException constraintViolationException) {
    Set<String> messages =
        constraintViolationException.getConstraintViolations().stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.toSet());

    ErrorResponse errorResponse = new ErrorResponse();
    errorResponse.setError("invalid_request");
    errorResponse.setErrorDescription(String.format("Validation failed on the request: %s", String.join(" | ", messages)));

    LOGGER.warn("An invalid request was rejected for reason: {}", message);
    return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
  }

  // other handlers
}

This is really useful, and helps map given exception(s) to a given HTTP response, and at the same time logs it for further diagnosis. But this can be improved two ways - firstly, we can take advantage of the base class's handleExceptionInternal method which provides a common way to map your exceptions to HTTP responses.

The great thing about this is that it's already in use by the built-in exceptions handled by ResponseEntityExceptionHandler method, so you're consistent with how Spring is doing it.

Therefore it's recommended that you modify your own code to do this, too:

import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import me.jvt.hacking.exception.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {

  private static final Logger LOGGER = LoggerFactory.getLogger(MicropubExceptionHandler.class);

  @ExceptionHandler(ConstraintViolationException.class)
  public final ResponseEntity<Object> constraintViolationException(
      ConstraintViolationException constraintViolationException, WebRequest request) {
    Set<String> messages =
        constraintViolationException.getConstraintViolations().stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.toSet());
    String message = String.format("Validation failed on the request: %s", String.join(" | ", messages));
    ErrorResponse errorResponse = new ErrorResponse();
    errorResponse.setError("invalid_request");
    errorResponse.setErrorDescription(message);

    LOGGER.warn("An invalid request was rejected for reason: {}", message);
    return handleExceptionInternal(
        constraintViolationException,
        errorResponse,
        new HttpHeaders(),
        HttpStatus.BAD_REQUEST,
        request);
  }

  // other handlers
}

This is great, but we still need to set up our logging each time, which can be a bit annoying. But even worse is the fact that even with this approach, we can't add logging for built-in exceptions like HttpMessageNotReadableException.

Hopefully you can see where this is going - because we're using handleExceptionInternal, we can override our implementation in our ExceptionHandler to perform logging based on the HTTP status code that's expected:

import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import me.jvt.hacking.exception.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {

  private static final Logger LOGGER = LoggerFactory.getLogger(MicropubExceptionHandler.class);

  @Override
  protected ResponseEntity<Object> handleExceptionInternal(
      @NonNull Exception ex,
      @Nullable Object body,
      @NonNull HttpHeaders headers,
      HttpStatus status,
      @NonNull WebRequest request) {
    if (status.is5xxServerError()) {
      LOGGER.error("An exception occured, which will cause a {} response", status, ex);
    } else if (status.is4xxClientError()){
      LOGGER.warn("An exception occured, which will cause a {} response", status, ex);
    } else {
      LOGGER.debug("An exception occured, which will cause a {} response", status, ex);
    }
    return super.handleExceptionInternal(ex, body, headers, status, request);
  }

  // handlers
}

This then gives us logging, everywhere, because we're now always delegating to handleExceptionInternal, and getting handy logs.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #spring #spring-boot.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.