Get all exceptions in Java and send them remotely

旧时模样 提交于 2019-12-05 02:27:29

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.

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);
    }

}

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

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 white 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.

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>

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.

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

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();
}

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!