14

I have a huge Java application. I want to intercept all Java Exceptions and send them by e-mail. I can't add everywhere code for sending the code via try-catch so is it possible to use for example Aspect to intercept the exception into low level classes and get the exception content?

Or is there some way to override some internal Java Class and get the exception payload?

What is possible?

arthur.sw
  • 11,052
  • 9
  • 47
  • 104
Peter Penzov
  • 1,126
  • 134
  • 430
  • 808
  • What type of application is this? A webapp, a desktop application, or something else? – hooknc Oct 06 '19 at 03:01
  • web based Spring-Boot application. – Peter Penzov Oct 06 '19 at 07:35
  • Perfect, thanks for the info. Another question, when an exception does occur, what is your desired outcome (besides to log it)? Do you want to show an error page to the end user? Ignore the exception and try to show what you can to the end user, etc... – hooknc Oct 06 '19 at 15:56
  • 1
    for example NPE from Java Object, from WebFlux connection. Logic errors which can lead to NPE and etc. – Peter Penzov Oct 06 '19 at 16:59
  • Not offering a solution, just a caution. I have found that Spring swallows some exceptions relatively quietly so be prepared for any solution to miss some exceptions using AOP or otherwise. – Jason K. Oct 07 '19 at 02:19
  • You want _all_ exceptions forwarded? What is the problem you need to solve? – Thorbjørn Ravn Andersen Oct 08 '19 at 14:45
  • I want to sent the exceptions remotely on order to track when issue occurs. – Peter Penzov Oct 08 '19 at 15:06
  • getting "all" exceptions is pretty bad idea, just catch the unhandled one (by using `Thread.setUncaughtExceptionHandler`) and send them + the spring request ones like in one of answers. As java applications throw a lot of exceptions that are just handed by code around it and are nothing to be interested in. Like even class loading often throw few exceptions before it load a class. – GotoFinal Oct 10 '19 at 09:53

8 Answers8

12

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.

burna
  • 2,932
  • 18
  • 27
  • Thanks, I will test it. – Peter Penzov Oct 05 '19 at 22:05
  • You mean, it will intercept all exceptions thrown by public methods of Spring components. Exceptions thrown by third-party classes which are not being escalated through to some Spring component will not be handled unless you use full AspectJ instead of Spring AOP. I am not a Spring user, but doesn't Spring provide its own exception handling mechanism via interceptors? – kriegaex Oct 06 '19 at 05:00
  • I don't know. I have also some custom Java code. I have to search for this. – Peter Penzov Oct 06 '19 at 07:36
  • Does this also catch exceptions that might occur in the spring filters (spring security, hibernate session per request filter, etc...)? It would be great if it does... – hooknc Oct 09 '19 at 18:34
4

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.

hooknc
  • 4,854
  • 5
  • 31
  • 60
4

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 = "foo@bar.com";
    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);
    }

}
Andrew Mairose
  • 10,615
  • 12
  • 60
  • 102
  • Can you please extend it with all possible exceptions to be catched by the method and send via mail, please? – Peter Penzov Oct 09 '19 at 20:42
  • The `@ExceptionHandler(value = Exception.class)` makes it execute the `defaultErrorHandler` method for ANY type of exception. If you are looking for only a specific type of exception, you can change that to a sub-class of `Exception.class`. – Andrew Mairose Oct 09 '19 at 20:45
  • Do you know is there some other additional information information that can be collected when there is a exception? – Peter Penzov Oct 09 '19 at 21:22
  • What other additional information are you looking for? This above example would include the logged-in user, the user-agent browser information, the URL that the error was triggered for, and the full stack trace of the exception. – Andrew Mairose Oct 14 '19 at 21:03
3

For Error handling read this

https://www.toptal.com/java/spring-boot-rest-api-error-handling

For Error Detail and Send email get the print track

public String printTraceMessage(Exception ex) {
    StringWriter errors = new StringWriter();
    ex.printStackTrace(new PrintWriter(errors));
    return errors.toString();
}

Or you can use the separate thread which non block the response and send the email

Nabeel Ahmed
  • 258
  • 1
  • 7
  • 23
3

The simplest way, how I will do it (if It is a web application) is create a filter and map it to all request and put a try-catch around filterChain.doFilter, and this would be single place to do desired stuff.

you can send mail using mail Appender of logger without writing any extra code. snippet from my log4j2.xml

  public class ApplicationErrorLoggingFilter extends OncePerRequestFilter{

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
            try {
                    filterChain.doFilter(requestCopier, responseCopier);

                }
            catch(Exception e){

                logger.error("Error Message",e)
                throw e;
            }

            finally {

            }
        }

 }

log4j2.xml

<Appenders>

    <SMTP name="MailAppender" subject="Error Alert on server"
        to="?" 
        from="?"
        smtpHost="smtp.gmail.com" smtpPort="465"
        smtpUsername="?" 
        smtpPassword="?"
        smtpProtocol="smtps"
        smtpDebug="true"
        bufferSize="1">
        <ThresholdFilter level="ERROR" onMatch="ACCEPT"
            onMismatch="DENY" />
        <PatternLayout>
            <Pattern>${MAIL_LOG_PATTERN}</Pattern>
        </PatternLayout>
    </SMTP>

</Appenders>
Shailesh Chandra
  • 2,164
  • 2
  • 17
  • 25
2

If you have all running threads in ht control you can mark them all with your implementation of Thread.UncaughtExceptionHandler. It might be a bit tricky if the app has deep multithread nature of course.

AlexGera
  • 756
  • 9
  • 19
  • As I said it depends on the app nature. You shall find all points where any thread created if any. If you have only single thread in your main just add the following: Thread.setDefaultUncaughtExceptionHandler(globalExceptionHandler); Of course it would work for only unhandled exceptions as named – AlexGera Sep 27 '19 at 23:05
2

You can follow these step to send error remotely. I'm using html by adding this into vm-file(Apache-Velocity-Template)

enter image description here

Api Demo

@RequestMapping(value = "/xyz", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public APIResponse xyz(@RequestBody String json) {
    Long startTime = System.currentTimeMillis();
    try {
    } catch (Exception ex) {
        logger.error("Error := " + ex);
        // add Constructor in ErrorVo
        // profileType mean the server like (staging|prod)
        ErrorVo apiError = new ErrorVo("/xyz", this.profileType, "XYZRestApi", "method-name", LocalDateTime.now(), this.extUtil.printTraceMessage(ex));
        this.extUtil.sendErrorEmail(apiError);
    }
    logger.info("Response Time :== {} ms ==:", System.currentTimeMillis() - startTime);
    return this.apiResponse;
}

Add these dependency into Pom.xml file

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity</artifactId>
    <version>1.7</version>
</dependency>

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-tools</artifactId>
    <version>2.0</version>
</dependency>

Add Html in error.vm and place under the resource/template folder

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
</head>
<body style="background:#ededed;padding:0;margin:20px 0;font-family: Calibri, sans-serif, serif, EmojiFont;">
<div style="border:1px solid #0056B3;background:#fff;width:650px;margin:0 auto;">

    <div style="width:100%;overflow:hidden;margin-bottom:10px;margin-top:10px;">
        <h1 style="color:#0056B3;font-size:16px;font-weight:bold;margin:10px 15px;">Api Break Alert.</h1>
        <hr style="border: 0;height: 0;border-top: 1px solid rgba(0, 0, 0, 0.1);border-bottom: 1px solid rgba(255, 255, 255, 0.3);margin:0 15px;" />
        <div style="overflow:hidden;margin-bottom:10px;margin:15px;">
        <p style="padding:0;margin:0;">Please Contact with the Support Team ASAP For Resolving the issue.</p>
      <table width="100%" border="0" align="center" cellpadding="0" cellspacing="0" style="width:100%;border:1pt solid #F1F1F1;margin-top:15px;">
          <tbody>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Contact Phone: <span style="font-weight:normal;">$request.getPhoneNumber()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Email: <span style="font-weight:normal;"><a href="#">$request.getEmails()</a></span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">End Point: <span style="font-weight:normal;">$request.getEndPoint()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Running On: <span style="font-weight:normal;">$request.getStage()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Service Type: <span style="font-weight:normal;">$request.getServiceType()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Method Name: <span style="font-weight:normal;">$request.getMethodName()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="30" align="left" valign="middle" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Exception Time: <span style="font-weight:normal;">$request.getExceptionTime()</span></p>
              </td>
            </tr>
            <tr>
              <td width="100%" height="100" align="left" valign="top" style="padding:3.75pt;border:1px solid #f1f1f1;font-size:14px;font-weight:bold;">
                  <p style="margin:0;padding:0;">Exception: <span style="font-weight:normal;">$request.getError()</span></p>
              </td>
            </tr>
          </tbody>
      </table>
      </div>
    </div>
</div>
</body>
</html>

Crete ErrorVo Class which have detail of error

public class ErrorVo {

    private String phoneNumber;
    private String emails;
    private String endPoint;
    private String stage;
    private String serviceType;
    private String methodName;
    private String exceptionTime;
    private String error;

    public ErrorVo() { }

    public ErrorVo(String endPoint, String stage, String serviceType, String methodName, String exceptionTime, String error) {
        this.endPoint = endPoint;
        this.stage = stage;
        this.serviceType = serviceType;
        this.methodName =  methodName;
        this.exceptionTime = exceptionTime;
        this.error = error;
    }

    public String getPhoneNumber() { return phoneNumber; }
    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber;     }

    public String getEmails() { return emails; }
    public void setEmails(String emails) { this.emails = emails; }

    public String getEndPoint() { return endPoint; }
    public void setEndPoint(String endPoint) { this.endPoint = endPoint; }

    public String getStage() { return stage; }
    public void setStage(String stage) { this.stage = stage; }

    public String getServiceType() { return serviceType; }
    public void setServiceType(String serviceType) { this.serviceType = serviceType; }

    public String getMethodName() { return methodName; }
    public void setMethodName(String methodName) { this.methodName = methodName; }

    public String getExceptionTime() { return exceptionTime; }
    public void setExceptionTime(String exceptionTime) { this.exceptionTime = exceptionTime; }

    public String getError() { return error; }
    public void setError(String error) { this.error = error; }

    @Override
    public String toString() { return new Gson().toJson(this); }

}

Add Templates Type

public enum TemplateType {
    ERROR_TEMPLATE
}

Add TemplateFactory Class which give the error.vm file

@Component
@Scope("prototype")
public class TemplateFactory {

    private Logger logger = LogManager.getLogger(TemplateFactory.class);

    public final String ERROR_TEMPLATE_PATH = "templates/error.vm";

    private Template template;
    private VelocityEngine engine;

    public TemplateFactory() { }

    public Template getTemplate(TemplateType templateType) {
        this.engine = this.getEngine();
        this.engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        this.engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        this.engine.init();
        switch (templateType) {
            case ERROR_TEMPLATE:
                logger.debug("Error-Template Path :- " + this.getERROR_TEMPLATE_PATH());
                this.template = this.engine.getTemplate(this.getERROR_TEMPLATE_PATH());
                break;
        }
        return template;
    }

    private VelocityEngine getEngine() { return new VelocityEngine(); }

    public String getERROR_TEMPLATE_PATH() { return ERROR_TEMPLATE_PATH; }

}

Add VelocityManager which get the vm file and write the error into vm file

@Component
@Scope("prototype")
public class VelocityManager {

    private final Logger logger = LogManager.getLogger(VelocityManager.class);

    @Autowired
    private TemplateFactory templateFactory;
    /*  create a context and add data */
    private VelocityContext context;
    /* now render the template into a StringWriter */
    private StringWriter writer;

    public VelocityContext getContext() { return context; }
    public void setContext(VelocityContext context) { this.context = context; }

    public String getResponseMessage(TemplateType templateType, Object object) throws Exception {
        String responseMessage = null;
        this.setWriter(new StringWriter());
        this.setContext(new VelocityContext());
        if(templateType.equals(ERROR_TEMPLATE)) {
            logger.info("Request Content :- " + object);
            this.context.put("request", (ErrorVo) object);
            responseMessage = this.getWriterResponse(templateType).toString();
        }
        return responseMessage;
    }

    private StringWriter getWriterResponse(TemplateType templateType) throws Exception {
        Template template = this.templateFactory.getTemplate(templateType);
        if(template != null) {
            template.merge(this.getContext(), this.getWriter());
            logger.info("Response Content :- " + this.getWriter().toString().replaceAll("\\s+",""));
            return this.getWriter();
        }
        throw new NullPointerException("Template Not Found");
    }

    public StringWriter getWriter() { return writer; }
    public void setWriter(StringWriter writer) { this.writer = writer; }
}

Create Some Util Class and add below methoe

public void sendErrorEmail(ErrorVo apiError) {
    String htmlWithErroDetail = this.velocityManager.getResponseMessage(ERROR_TEMPLATE, apiError);
    // Note :- Now you have html with error. i'm using aws-ses email. you go with your option like (java-email, aws-ses, sendgrid)

}

public String printTraceMessage(Exception ex) {
    StringWriter errors = new StringWriter();
    ex.printStackTrace(new PrintWriter(errors));
    return errors.toString();
}
Community
  • 1
  • 1
Nabeel Ahmed
  • 258
  • 1
  • 7
  • 23
1

It is possible to implement your own java.lang.Throwable class. To get the JVM to use it, the JVM bootclasspath has to be set when starting the process. Example with Java 8 on Windows:

java.exe -Xbootclasspath/p:C:\..\ReplceJavaLangClasses\bin -classpath ... MyApp

In this example the folder C:\..\ReplaceJavaLangClasses\bin contains the class of the modified copy of the original java.lang Throwable.java code, as usual in the proper package sub folder java/lang/Throwable.class. Now you can add your own exception management doing things, for example:

  ...
  public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
    System.out.println("################ my additional code ##############");
 }

With modifying all constructors you can for example react on all instanciations of exceptions.

geri
  • 346
  • 2
  • 6