Get all exceptions in Java and send them remotely

Solution 1:

You can use the @AfterThrowing advice of spring-aop.

@Aspect
@Component
public class MailExceptionAspect {

    @AfterThrowing(value="execution(* com.example..*.*(..))", throwing="ex" )
    public void mailAfterThrowing(Throwable ex) {
        // do something to send an email
    }
}

This will intercept all exceptions, that are not handled, in the package com.example. Beware, that exceptions that are handled (caught) in the application, can not be intercepted.

Another solution would be to use the logging framework of the application. Many frameworks, like logback, log4j provide builtin configurations that can send logs by email.

Solution 2:

Look into Spring's @ControllerAdvice annotation. We use that to do exactly what I think you want. We have a web application that has a number of @Controllers and @RestControllers. This will send an email with a number of details about the request that triggered it whenever an error is thrown by any method in those controllers. We don't send emails for ClientAbortExceptions, as those occur often when a user closes their browser while a request is being processed.

@ControllerAdvice
public class GlobalExceptionHandler {

    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    private static final String ERROR_EMAIL_ADDRESS = "[email protected]";
    private static final String APPLICATION_ERROR_SUBJECT = "Foo Error Occurred";
    private static final String USER_AGENT = "user-agent";

    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseEntity defaultErrorHandler(final HttpServletRequest request, final Principal principal, final Exception e) {
        final String userTime = principal.getName() + " triggered an error at " + new Date();
        final String userAgent = "User-Agent: " + StringUtils.trimToEmpty(request.getHeader(USER_AGENT));
        final String url = "URL: " + StringUtils.trimToEmpty(request.getRequestURL().toString());
        final String httpMethod = "HTTP method: " + request.getMethod();

        final StringBuilder emailSb = new StringBuilder();
        emailSb.append(userTime).append("\n");
        emailSb.append(userAgent).append("\n");
        emailSb.append(url).append("\n");
        emailSb.append(httpMethod).append("\n");

        if(e instanceof ClientAbortException){
            logger.debug("Not sending email for socketExceptions");
        }else {
            emailSb.append(ExceptionUtils.getStackTrace(e));
            //just a simple util class we use to send emails with javax.mail api
            EmailUtil.sendEmail(ERROR_EMAIL_ADDRESS, ERROR_EMAIL_ADDRESS, APPLICATION_ERROR_SUBJECT,
                                emailSb.toString());
        }

        return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
    }

}

Solution 3:

So, this is what we do with our Spring based webapp.

To catch all unintended exceptions, we have an exception servlet filter that is the very first/last filter in the filter chain.

This filter will catch any exception and then send us an email. BTW, we have a ignore list of exceptions that we don't report. Think client abort exceptions. For us, there really isn't any reason to report those.

For tasks that happen due to a user request, but shouldn't interfere with a user's result, we wrap those actions with a try/catch and then will send an email if that side action fails.

An example of a side action would be to update the search index if someone saves new data to the database. The end user just wants to know that their item was saved successfully to the database, but they don't need to know that the update to the search index failed. We (the developers do), but in general, the end user doesn't care.

Then for backend tasks that require their own threads, we have created a thread that does a try/catch statement and will send an email if an exception is thrown.

A example of a task like this is reindexing your search index. That can be a long running process and we don't want to keep an http connection open for the entire time that process is running, so we create a new thread for the reindexing to run in. If something goes wrong, we want to know about it.

Here is some example code to show you how we implement our services...

@Transactional
public UUID saveRecord(RecordRequest recordRequest) {

    Record newRecord = this.recordFactory.create(recordRequest);

    this.recordRepository.add(newRecord);

    this.updateSearch(newRecord);
}

private void updateSearch(Record record) {

    try {

        this.searchIndex.add(record);

    catch(Exception e) {

        this.errorService.reportException(e);
    }
}

Here is the code for our exception handling filter:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

    try {

        filterChain.doFilter(request, response);

    } catch (Throwable exception) {

        this.handleException(request, response, exception);
    }
}

private void handleException(ServletRequest request, ServletResponse response, Throwable throwable) {

    try {

        this.doHandleException(request, response, throwable);

    } catch (Exception handlingException) {

        LOG.error("This exception that was not handled by the UnhandledExceptionFilter", throwable);
        LOG.error("This exception occurred reporting an unhandled exception, please see the 'cause by' exception above", handlingException);
    }
}

private void doHandleException(ServletRequest request, ServletResponse response, Throwable throwable) throws Exception {

    this.errorResponse.send(request, response);

    this.reportException(request, response, throwable);

}

/**
 * Report exception.
 *
 * @param request   the request
 * @param response  the response
 * @param throwable the throwable
 */
protected void reportException(ServletRequest request, ServletResponse response, Throwable throwable) {

    UnhandledException unhandledException = this.setupExceptionDetails((HttpServletRequest) request, (HttpServletResponse) response, throwable);

    this.exceptionHandlingService.handleUnexpectedException(unhandledException);
}

private UnhandledException setupExceptionDetails(HttpServletRequest request, HttpServletResponse response, Throwable throwable) {

    UnhandledException unhandledException = new UnhandledException(throwable);

    if (response.isCommitted()) {
        unhandledException.put("Session Id", "response already committed, cannot get Session Id");
    } else {
        unhandledException.put("Session Id", request.getSession().getId());
    }
    unhandledException.put("Remote Address", request.getRemoteAddr());
    unhandledException.put("User Agent", request.getHeader(HttpHeaderConstants.USER_AGENT));
    unhandledException.put("Server Name", request.getServerName());
    unhandledException.put("Server Port", "" + request.getServerPort());
    unhandledException.put("Method", request.getMethod());
    unhandledException.put("URL", request.getRequestURI());
    unhandledException.put("Referer", request.getHeader(HttpHeaderConstants.REFERRER));

    Cookie[] cookies = request.getCookies();

    if (cookies != null && cookies.length != 0) {

        for (Cookie cookie : cookies) {

            unhandledException.put(cookie.getName(), cookie.getValue());
        }
    }

    unhandledException.put("Query String", request.getQueryString());

    Enumeration parameterNames = request.getParameterNames();

    while (parameterNames.hasMoreElements()) {

        String parameterName = (String) parameterNames.nextElement();

        String parameterValue = request.getParameter(parameterName);

        if (parameterName.equals("j_password") || parameterName.equals("password") || parameterName.equals("confirmationPassword") || parameterName.equals("oldPassword") || parameterName.equals("confirmNewPassword")) {

            parameterValue = "********";
        }

        unhandledException.put(parameterName, "'" + parameterValue + "'");
    }

    return unhandledException;
}

BTW, when sending yourself email from a production service, it is significantly important to rate limit the numbers of emails that your service sends in a minute and that there is a way of bundling the same types of exception into one emails.

It is not fun receiving a phone call from your managers, manager, manager, where they tell you that you have to stop the DOS (denial of service) attack on the company's email server. Twice...

We solved this problem by using Spring Integration (with activemq backed queues) to limit the number of emails sent.

Then we used a counting strategy to track how many of the same exception are being sent and then try to bundle those emails into one email with the count of how many times that particular exception occurs.