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.