Home > Mobile >  Best way to log the controller requests on error response
Best way to log the controller requests on error response

Time:12-12

Pesudocode :-

@RestController 
class Controller {

    @GetMapping()
    public ResponseEntity<?> getAPI(@Valid @RequestBody Request request) {
        try {
            // success response.
        } catch (RequestParamsException e) {
            // log.error(e   request) log error and request.
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        } catch (Exception e) {
            // log.error(e   request) log error and request
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

}

We want to log the request for all error logs at the controller level. Simplest approach is that we can go to every api in all controllers and log the request there, Is there any better way we can do this for all APIs in spring using AOP/filters/interceptors somehow.

CodePudding user response:

Try to use error handling and @ControllerAdvice

There you can have your common method to log errors

CodePudding user response:

Yes, there is a better way! You could define a bean as follows:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {

}

In this class, you can add exception handlers for Spring exceptions, e.g.

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
        final String message = Optional.ofNullable(ex.getRequiredType())
            .map(Class::getName)
            .map(className -> String.format("%s should be of type %s", ex.getName(), className))
            .orElseGet(ex::getMessage);
        return handleExceptionInternal(ex, message, BAD_REQUEST, request);
    }

You can also add exception handlers for custom exceptions, e.g.

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<Object> handleNotFoundException(Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, NOT_FOUND, request);
    }

where all exceptions you throw when an entity can't be found in the DB extend the NotFoundException.

You may also want to override certain methods from the ResponseEntityExceptionHandler super class, e.g.

    @NotNull
    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
                                                                          @NotNull HttpHeaders headers,
                                                                          @NotNull HttpStatus status,
                                                                          @NotNull WebRequest request) {
        final String message = String.format("%s parameter is missing", ex.getParameterName());
        return handleExceptionInternal(ex, message, BAD_REQUEST, request);
    }

A fallback for all other uncatched exceptions:

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, "An internal error occurred", INTERNAL_SERVER_ERROR, request);
    }

Now to the logging part: you need to override the following method in order to log your errors in a custom way, please note that I return a special DTO ApiError when an exception is thrown:

    @Override
    @NotNull
    protected ResponseEntity<Object> handleExceptionInternal(@NotNull Exception ex,
                                                             @Nullable Object body,
                                                             @NotNull HttpHeaders headers,
                                                             @NotNull HttpStatus status,
                                                             @NotNull WebRequest webRequest) {
        // Log
        final String errorId = UUID.randomUUID().toString();
        final HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
        final String path = String.format("%s %s", request.getMethod(), request.getRequestURI());
        final BiConsumer<String, Exception> log = status.is4xxClientError() ? logger::info : logger::error;
        log.accept(String.format("%s returned for request %s, errorId is %s", status, path, errorId), ex);

        // Create API error
        final ApiError apiError = ApiError.builder()
            .errorId(errorId)
            .message(Optional.ofNullable(body)
                .filter(String.class::isInstance)
                .map(String.class::cast)
                .orElseGet(ex::getMessage))
            .build();

        // Return
        return new ResponseEntity<>(apiError, headers, status);
    }

Depending on whether it is a 4xx or a 5xx error, I either log on info or error level. The @ControllerAdvice annotation instructs Spring to use this bean as exception handler for all Controllers, thereby being your global error handler.

Everything from above puzzled together (plus helper methods I use for the examples):

@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {

    // ---------------
    // 400 Bad Request
    // ---------------

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
        final String message = Optional.ofNullable(ex.getRequiredType())
            .map(Class::getName)
            .map(className -> String.format("%s should be of type %s", ex.getName(), className))
            .orElseGet(ex::getMessage);
        return handleExceptionInternal(ex, message, BAD_REQUEST, request);
    }

    // ---------------
    // 404 Bad Request
    // ---------------

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<Object> handleNotFoundException(Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, NOT_FOUND, request);
    }

    // -------------------------
    // 500 Internal Server Error
    // -------------------------

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, "An internal error occurred", INTERNAL_SERVER_ERROR, request);
    }

    // --------------
    // Helper methods
    // --------------

    private ResponseEntity<Object> handleExceptionInternal(Exception ex, HttpStatus httpStatus, WebRequest request) {
        return handleExceptionInternal(ex, ex.getMessage(), httpStatus, request);
    }

    private ResponseEntity<Object> handleExceptionInternal(Exception ex, String errorMessage, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, errorMessage, new HttpHeaders(), status, request);
    }

    @Override
    @NotNull
    protected ResponseEntity<Object> handleExceptionInternal(@NotNull Exception ex,
                                                             @Nullable Object body,
                                                             @NotNull HttpHeaders headers,
                                                             @NotNull HttpStatus status,
                                                             @NotNull WebRequest webRequest) {
        // Log
        final String errorId = UUID.randomUUID().toString();
        final HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
        final String path = String.format("%s %s", request.getMethod(), request.getRequestURI());
        final BiConsumer<String, Exception> log = status.is4xxClientError() ? logger::info : logger::error;
        log.accept(String.format("%s returned for request %s, errorId is %s", status, path, errorId), ex);

        // Create API error
        final ApiError apiError = ApiError.builder()
            .errorId(errorId)
            .message(Optional.ofNullable(body)
                .filter(String.class::isInstance)
                .map(String.class::cast)
                .orElseGet(ex::getMessage))
            .build();

        // Return
        return new ResponseEntity<>(apiError, headers, status);
    }

}

Feel free to add more custom exceptions or Spring exceptions that occurr in the web layer and check out whether you want to override additional ResponseEntityExceptionHandler methods like handleMethodArgumentNotValid!

  • Related