Exception Notification System in Java

Introduction

A comprehensive Exception Notification System that captures, processes, and notifies about exceptions through multiple channels (email, Slack, PagerDuty, etc.) with intelligent grouping, filtering, and escalation policies.

Core Architecture

Exception Event Model

package com.exceptionnotifier;
import java.time.Instant;
import java.util.*;
public class ExceptionEvent {
private final String id;
private final Instant timestamp;
private final Throwable throwable;
private final String message;
private final Map<String, Object> context;
private final String threadName;
private final StackTraceElement[] stackTrace;
private final String applicationName;
private final String environment;
private final Severity severity;
private int occurrenceCount = 1;
public enum Severity {
LOW, MEDIUM, HIGH, CRITICAL
}
public ExceptionEvent(Throwable throwable, String message, Map<String, Object> context, 
String applicationName, String environment) {
this.id = UUID.randomUUID().toString();
this.timestamp = Instant.now();
this.throwable = throwable;
this.message = message != null ? message : throwable.getMessage();
this.context = context != null ? new HashMap<>(context) : new HashMap<>();
this.threadName = Thread.currentThread().getName();
this.stackTrace = throwable.getStackTrace();
this.applicationName = applicationName;
this.environment = environment;
this.severity = calculateSeverity(throwable);
// Add automatic context
this.context.putIfAbsent("hostname", getHostname());
this.context.putIfAbsent("user", System.getProperty("user.name"));
}
private Severity calculateSeverity(Throwable throwable) {
if (throwable instanceof OutOfMemoryError) {
return Severity.CRITICAL;
} else if (throwable instanceof NullPointerException) {
return Severity.MEDIUM;
} else if (throwable instanceof IllegalArgumentException) {
return Severity.LOW;
} else if (throwable instanceof RuntimeException) {
return Severity.HIGH;
}
return Severity.MEDIUM;
}
// Getters
public String getId() { return id; }
public Instant getTimestamp() { return timestamp; }
public Throwable getThrowable() { return throwable; }
public String getMessage() { return message; }
public Map<String, Object> getContext() { return Collections.unmodifiableMap(context); }
public String getThreadName() { return threadName; }
public StackTraceElement[] getStackTrace() { return stackTrace.clone(); }
public String getApplicationName() { return applicationName; }
public String getEnvironment() { return environment; }
public Severity getSeverity() { return severity; }
public int getOccurrenceCount() { return occurrenceCount; }
public void incrementOccurrenceCount() { this.occurrenceCount++; }
public String getExceptionType() {
return throwable.getClass().getName();
}
public String getRootCause() {
Throwable root = throwable;
while (root.getCause() != null) {
root = root.getCause();
}
return root.getClass().getName();
}
private String getHostname() {
try {
return java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
return "unknown";
}
}
@Override
public String toString() {
return String.format("ExceptionEvent{id='%s', type='%s', message='%s', severity=%s}", 
id, getExceptionType(), message, severity);
}
}

Exception Capture Service

package com.exceptionnotifier;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
public class ExceptionCaptureService {
private final List<ExceptionHandler> handlers;
private final ExceptionGroupingService groupingService;
private final ExecutorService executor;
private final AtomicLong exceptionsCaptured;
private final Set<String> suppressedExceptions;
private final RateLimiter rateLimiter;
public ExceptionCaptureService() {
this.handlers = new CopyOnWriteArrayList<>();
this.groupingService = new ExceptionGroupingService();
this.executor = Executors.newFixedThreadPool(4);
this.exceptionsCaptured = new AtomicLong(0);
this.suppressedExceptions = ConcurrentHashMap.newKeySet();
this.rateLimiter = new RateLimiter(100, TimeUnit.MINUTES); // 100 exceptions per minute
}
public void captureException(Throwable throwable) {
captureException(throwable, null, null, null);
}
public void captureException(Throwable throwable, String message) {
captureException(throwable, message, null, null);
}
public void captureException(Throwable throwable, String message, 
Map<String, Object> context, String applicationName) {
if (shouldSuppress(throwable)) {
return;
}
if (!rateLimiter.tryAcquire()) {
System.err.println("Rate limit exceeded for exception notifications");
return;
}
exceptionsCaptured.incrementAndGet();
String application = applicationName != null ? applicationName : 
System.getProperty("application.name", "unknown");
String environment = System.getProperty("application.env", "development");
ExceptionEvent event = new ExceptionEvent(throwable, message, context, 
application, environment);
// Group similar exceptions
String groupKey = groupingService.groupException(event);
event = groupingService.getGroupedEvent(event, groupKey);
// Process asynchronously
executor.submit(() -> processException(event));
}
private boolean shouldSuppress(Throwable throwable) {
String exceptionName = throwable.getClass().getName();
// Check if this exception type is suppressed
if (suppressedExceptions.contains(exceptionName)) {
return true;
}
// Suppress certain exception types by default
return exceptionName.contains("IgnoredException") ||
exceptionName.contains("ExpectedException") ||
isBusinessException(throwable);
}
private boolean isBusinessException(Throwable throwable) {
// Business exceptions that don't need notification
return throwable.getClass().getSimpleName().contains("Business") ||
throwable.getClass().getSimpleName().contains("Validation");
}
private void processException(ExceptionEvent event) {
try {
for (ExceptionHandler handler : handlers) {
if (handler.shouldHandle(event)) {
handler.handleException(event);
}
}
} catch (Exception e) {
System.err.println("Error processing exception: " + e.getMessage());
// Don't let handler exceptions break the system
}
}
public void addHandler(ExceptionHandler handler) {
handlers.add(handler);
}
public void removeHandler(ExceptionHandler handler) {
handlers.remove(handler);
}
public void suppressException(String exceptionClassName) {
suppressedExceptions.add(exceptionClassName);
}
public void unsuppressException(String exceptionClassName) {
suppressedExceptions.remove(exceptionClassName);
}
public long getExceptionsCaptured() {
return exceptionsCaptured.get();
}
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Rate limiter implementation
private static class RateLimiter {
private final int maxPermits;
private final long periodNanos;
private long nextFreeNanos;
private final Object lock = new Object();
public RateLimiter(int maxPermits, TimeUnit timeUnit) {
this.maxPermits = maxPermits;
this.periodNanos = timeUnit.toNanos(1);
this.nextFreeNanos = System.nanoTime();
}
public boolean tryAcquire() {
synchronized (lock) {
long now = System.nanoTime();
if (now > nextFreeNanos) {
// Reset if period has passed
nextFreeNanos = now + periodNanos;
return true;
}
// Calculate if we can acquire within rate limit
long elapsed = nextFreeNanos - now;
double permitsUsed = (double) periodNanos / elapsed;
if (permitsUsed < maxPermits) {
nextFreeNanos += periodNanos / maxPermits;
return true;
}
return false;
}
}
}
}

Exception Grouping and Deduplication

Intelligent Exception Grouping

package com.exceptionnotifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ExceptionGroupingService {
private final ConcurrentMap<String, ExceptionGroup> exceptionGroups;
private final long groupTimeoutMs;
public ExceptionGroupingService() {
this(TimeUnit.MINUTES.toMillis(30)); // 30 minute grouping window
}
public ExceptionGroupingService(long groupTimeoutMs) {
this.exceptionGroups = new ConcurrentHashMap<>();
this.groupTimeoutMs = groupTimeoutMs;
}
public String groupException(ExceptionEvent event) {
String fingerprint = generateFingerprint(event);
ExceptionGroup group = exceptionGroups.computeIfAbsent(fingerprint, 
k -> new ExceptionGroup(fingerprint, event));
group.addOccurrence(event);
cleanupExpiredGroups();
return fingerprint;
}
public ExceptionEvent getGroupedEvent(ExceptionEvent event, String groupKey) {
ExceptionGroup group = exceptionGroups.get(groupKey);
if (group != null) {
event = group.createGroupedEvent();
}
return event;
}
private String generateFingerprint(ExceptionEvent event) {
// Create fingerprint based on exception type and stack trace
StringBuilder fingerprint = new StringBuilder();
// Exception type
fingerprint.append(event.getExceptionType());
// Root cause
fingerprint.append("::").append(event.getRootCause());
// Top stack trace elements (first 3)
StackTraceElement[] stackTrace = event.getStackTrace();
for (int i = 0; i < Math.min(3, stackTrace.length); i++) {
fingerprint.append("::")
.append(stackTrace[i].getClassName())
.append(".")
.append(stackTrace[i].getMethodName())
.append(":")
.append(stackTrace[i].getLineNumber());
}
return fingerprint.toString();
}
private void cleanupExpiredGroups() {
long now = System.currentTimeMillis();
exceptionGroups.entrySet().removeIf(entry -> 
now - entry.getValue().getLastOccurrence() > groupTimeoutMs);
}
public Map<String, ExceptionGroup> getExceptionGroups() {
return new HashMap<>(exceptionGroups);
}
public void clearGroups() {
exceptionGroups.clear();
}
public static class ExceptionGroup {
private final String fingerprint;
private final AtomicInteger occurrenceCount;
private final ExceptionEvent firstOccurrence;
private volatile long lastOccurrence;
private volatile ExceptionEvent lastEvent;
public ExceptionGroup(String fingerprint, ExceptionEvent firstOccurrence) {
this.fingerprint = fingerprint;
this.occurrenceCount = new AtomicInteger(1);
this.firstOccurrence = firstOccurrence;
this.lastOccurrence = System.currentTimeMillis();
this.lastEvent = firstOccurrence;
}
public void addOccurrence(ExceptionEvent event) {
occurrenceCount.incrementAndGet();
lastOccurrence = System.currentTimeMillis();
lastEvent = event;
}
public ExceptionEvent createGroupedEvent() {
// Create a new event that represents the group
ExceptionEvent groupedEvent = new ExceptionEvent(
lastEvent.getThrowable(),
String.format("%s (occurred %d times)", 
lastEvent.getMessage(), occurrenceCount.get()),
lastEvent.getContext(),
lastEvent.getApplicationName(),
lastEvent.getEnvironment()
);
// Set the occurrence count
for (int i = 1; i < occurrenceCount.get(); i++) {
groupedEvent.incrementOccurrenceCount();
}
return groupedEvent;
}
// Getters
public String getFingerprint() { return fingerprint; }
public int getOccurrenceCount() { return occurrenceCount.get(); }
public ExceptionEvent getFirstOccurrence() { return firstOccurrence; }
public long getLastOccurrence() { return lastOccurrence; }
public ExceptionEvent getLastEvent() { return lastEvent; }
}
}

Notification Handlers

Handler Interface and Base Implementation

package com.exceptionnotifier;
import java.util.concurrent.atomic.AtomicLong;
public interface ExceptionHandler {
boolean shouldHandle(ExceptionEvent event);
void handleException(ExceptionEvent event);
String getName();
void shutdown();
}
abstract class AbstractExceptionHandler implements ExceptionHandler {
protected final String name;
protected final AtomicLong handledCount;
protected volatile boolean enabled = true;
protected AbstractExceptionHandler(String name) {
this.name = name;
this.handledCount = new AtomicLong(0);
}
@Override
public boolean shouldHandle(ExceptionEvent event) {
return enabled && isHandlerEnabledForEvent(event);
}
protected abstract boolean isHandlerEnabledForEvent(ExceptionEvent event);
@Override
public void handleException(ExceptionEvent event) {
if (shouldHandle(event)) {
handledCount.incrementAndGet();
performHandle(event);
}
}
protected abstract void performHandle(ExceptionEvent event);
@Override
public String getName() {
return name;
}
@Override
public void shutdown() {
enabled = false;
}
public long getHandledCount() {
return handledCount.get();
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
}

Email Notification Handler

package com.exceptionnotifier;
import javax.mail.*;
import javax.mail.internet.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EmailNotificationHandler extends AbstractExceptionHandler {
private final String smtpHost;
private final int smtpPort;
private final String username;
private final String password;
private final boolean useSSL;
private final String fromAddress;
private final List<String> toAddresses;
private final ExecutorService emailExecutor;
private final Set<String> notifiedFingerprints;
private final long notificationCooldownMs;
public EmailNotificationHandler(String name, String smtpHost, int smtpPort, 
String username, String password, boolean useSSL,
String fromAddress, List<String> toAddresses) {
super(name);
this.smtpHost = smtpHost;
this.smtpPort = smtpPort;
this.username = username;
this.password = password;
this.useSSL = useSSL;
this.fromAddress = fromAddress;
this.toAddresses = new ArrayList<>(toAddresses);
this.emailExecutor = Executors.newSingleThreadExecutor();
this.notifiedFingerprints = Collections.synchronizedSet(new HashSet<>());
this.notificationCooldownMs = TimeUnit.MINUTES.toMillis(30); // 30 min cooldown
}
@Override
protected boolean isHandlerEnabledForEvent(ExceptionEvent event) {
// Only handle medium and above severity
return event.getSeverity().ordinal() >= ExceptionEvent.Severity.MEDIUM.ordinal();
}
@Override
protected void performHandle(ExceptionEvent event) {
String fingerprint = generateEventFingerprint(event);
// Check cooldown
if (!shouldNotify(fingerprint)) {
return;
}
emailExecutor.submit(() -> sendEmailNotification(event, fingerprint));
}
private boolean shouldNotify(String fingerprint) {
synchronized (notifiedFingerprints) {
if (notifiedFingerprints.contains(fingerprint)) {
return false;
}
notifiedFingerprints.add(fingerprint);
// Schedule removal after cooldown
Timer timer = new Timer(true);
timer.schedule(new TimerTask() {
@Override
public void run() {
notifiedFingerprints.remove(fingerprint);
}
}, notificationCooldownMs);
return true;
}
}
private String generateEventFingerprint(ExceptionEvent event) {
return event.getExceptionType() + "::" + event.getMessage().hashCode();
}
private void sendEmailNotification(ExceptionEvent event, String fingerprint) {
try {
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost);
props.put("mail.smtp.port", String.valueOf(smtpPort));
props.put("mail.smtp.auth", "true");
if (useSSL) {
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.port", String.valueOf(smtpPort));
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
} else {
props.put("mail.smtp.starttls.enable", "true");
}
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(fromAddress));
for (String to : toAddresses) {
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
}
message.setSubject(buildSubject(event));
message.setContent(buildEmailContent(event), "text/html; charset=utf-8");
Transport.send(message);
System.out.println("Email notification sent for: " + event.getMessage());
} catch (Exception e) {
System.err.println("Failed to send email notification: " + e.getMessage());
}
}
private String buildSubject(ExceptionEvent event) {
return String.format("[%s] %s - %s", 
event.getSeverity(), 
event.getApplicationName(), 
event.getExceptionType());
}
private String buildEmailContent(ExceptionEvent event) {
StringBuilder html = new StringBuilder();
html.append("""
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f0f0f0; padding: 15px; border-radius: 5px; }
.section { margin: 15px 0; padding: 10px; border-left: 4px solid #007cba; }
.stacktrace { background: #f8f8f8; padding: 10px; border-radius: 3px; font-family: monospace; }
.context-table { border-collapse: collapse; width: 100%; }
.context-table th, .context-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.context-table th { background-color: #f2f2f2; }
</style>
</head>
<body>
""");
html.append("<div class='header'>")
.append("<h2>Exception Notification</h2>")
.append("</div>");
// Basic info
html.append("<div class='section'>")
.append("<h3>Exception Details</h3>")
.append("<p><strong>Application:</strong> ").append(event.getApplicationName()).append("</p>")
.append("<p><strong>Environment:</strong> ").append(event.getEnvironment()).append("</p>")
.append("<p><strong>Severity:</strong> ").append(event.getSeverity()).append("</p>")
.append("<p><strong>Occurrences:</strong> ").append(event.getOccurrenceCount()).append("</p>")
.append("<p><strong>Time:</strong> ").append(event.getTimestamp()).append("</p>")
.append("</div>");
// Exception info
html.append("<div class='section'>")
.append("<h3>Exception Information</h3>")
.append("<p><strong>Type:</strong> ").append(event.getExceptionType()).append("</p>")
.append("<p><strong>Message:</strong> ").append(event.getMessage()).append("</p>")
.append("<p><strong>Root Cause:</strong> ").append(event.getRootCause()).append("</p>")
.append("</div>");
// Stack trace
html.append("<div class='section'>")
.append("<h3>Stack Trace</h3>")
.append("<div class='stacktrace'>");
for (StackTraceElement element : event.getStackTrace()) {
html.append(element.toString()).append("<br/>");
}
html.append("</div></div>");
// Context
if (!event.getContext().isEmpty()) {
html.append("<div class='section'>")
.append("<h3>Context Information</h3>")
.append("<table class='context-table'>")
.append("<tr><th>Key</th><th>Value</th></tr>");
for (Map.Entry<String, Object> entry : event.getContext().entrySet()) {
html.append("<tr><td>").append(entry.getKey()).append("</td>")
.append("<td>").append(entry.getValue()).append("</td></tr>");
}
html.append("</table></div>");
}
html.append("</body></html>");
return html.toString();
}
@Override
public void shutdown() {
super.shutdown();
emailExecutor.shutdown();
try {
if (!emailExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
emailExecutor.shutdownNow();
}
} catch (InterruptedException e) {
emailExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

Slack Notification Handler

package com.exceptionnotifier;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SlackNotificationHandler extends AbstractExceptionHandler {
private final String webhookUrl;
private final String channel;
private final String username;
private final String iconEmoji;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final ExecutorService slackExecutor;
public SlackNotificationHandler(String name, String webhookUrl, String channel, 
String username, String iconEmoji) {
super(name);
this.webhookUrl = webhookUrl;
this.channel = channel;
this.username = username;
this.iconEmoji = iconEmoji;
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper();
this.slackExecutor = Executors.newFixedThreadPool(2);
}
@Override
protected boolean isHandlerEnabledForEvent(ExceptionEvent event) {
// Only handle high and critical severity for Slack
return event.getSeverity().ordinal() >= ExceptionEvent.Severity.HIGH.ordinal();
}
@Override
protected void performHandle(ExceptionEvent event) {
slackExecutor.submit(() -> sendSlackNotification(event));
}
private void sendSlackNotification(ExceptionEvent event) {
try {
Map<String, Object> slackMessage = buildSlackMessage(event);
String jsonMessage = objectMapper.writeValueAsString(slackMessage);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(webhookUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonMessage))
.build();
CompletableFuture<HttpResponse<String>> response = 
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
response.thenAccept(resp -> {
if (resp.statusCode() >= 400) {
System.err.println("Slack notification failed: " + resp.body());
} else {
System.out.println("Slack notification sent for: " + event.getMessage());
}
});
} catch (Exception e) {
System.err.println("Failed to send Slack notification: " + e.getMessage());
}
}
private Map<String, Object> buildSlackMessage(ExceptionEvent event) {
Map<String, Object> message = new HashMap<>();
message.put("channel", channel);
message.put("username", username);
message.put("icon_emoji", iconEmoji);
// Determine color based on severity
String color = getColorForSeverity(event.getSeverity());
// Build attachments
List<Map<String, Object>> attachments = new ArrayList<>();
Map<String, Object> attachment = new HashMap<>();
attachment.put("color", color);
attachment.put("title", "Exception Occurred");
attachment.put("text", event.getMessage());
List<Map<String, Object>> fields = new ArrayList<>();
// Add fields
fields.add(createField("Application", event.getApplicationName(), true));
fields.add(createField("Environment", event.getEnvironment(), true));
fields.add(createField("Severity", event.getSeverity().toString(), true));
fields.add(createField("Exception Type", event.getExceptionType(), false));
fields.add(createField("Occurrences", String.valueOf(event.getOccurrenceCount()), true));
attachment.put("fields", fields);
// Add stack trace as a separate attachment
if (event.getStackTrace().length > 0) {
String stackTrace = getShortStackTrace(event);
Map<String, Object> stackTraceAttachment = new HashMap<>();
stackTraceAttachment.put("color", color);
stackTraceAttachment.put("title", "Stack Trace (First 5 lines)");
stackTraceAttachment.put("text", "```" + stackTrace + "```");
attachments.add(stackTraceAttachment);
}
attachments.add(attachment);
message.put("attachments", attachments);
return message;
}
private Map<String, Object> createField(String title, String value, boolean shortField) {
Map<String, Object> field = new HashMap<>();
field.put("title", title);
field.put("value", value);
field.put("short", shortField);
return field;
}
private String getColorForSeverity(ExceptionEvent.Severity severity) {
switch (severity) {
case CRITICAL: return "#ff0000"; // Red
case HIGH: return "#ff9900";     // Orange
case MEDIUM: return "#ffff00";   // Yellow
case LOW: return "#36a64f";      // Green
default: return "#cccccc";       // Gray
}
}
private String getShortStackTrace(ExceptionEvent event) {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = event.getStackTrace();
for (int i = 0; i < Math.min(5, stackTrace.length); i++) {
sb.append(stackTrace[i].toString()).append("\n");
}
if (stackTrace.length > 5) {
sb.append("... and ").append(stackTrace.length - 5).append(" more");
}
return sb.toString();
}
@Override
public void shutdown() {
super.shutdown();
slackExecutor.shutdown();
}
}

Logging Handler

package com.exceptionnotifier;
import java.util.logging.*;
public class LoggingHandler extends AbstractExceptionHandler {
private final Logger logger;
public LoggingHandler(String name) {
super(name);
this.logger = Logger.getLogger("ExceptionNotifier");
}
@Override
protected boolean isHandlerEnabledForEvent(ExceptionEvent event) {
return true; // Log all exceptions
}
@Override
protected void performHandle(ExceptionEvent event) {
Level logLevel = getLogLevel(event.getSeverity());
String logMessage = String.format(
"[%s] %s - %s (Occurrences: %d)", 
event.getSeverity(),
event.getExceptionType(),
event.getMessage(),
event.getOccurrenceCount()
);
logger.log(logLevel, logMessage, event.getThrowable());
// Log context if available
if (!event.getContext().isEmpty()) {
logger.log(logLevel, "Context: " + event.getContext());
}
}
private Level getLogLevel(ExceptionEvent.Severity severity) {
switch (severity) {
case CRITICAL:
case HIGH:
return Level.SEVERE;
case MEDIUM:
return Level.WARNING;
case LOW:
return Level.INFO;
default:
return Level.FINE;
}
}
}

Integration with Applications

Spring Boot Integration

package com.exceptionnotifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.util.Arrays;
import java.util.List;
@Configuration
public class ExceptionNotifierConfig {
@Value("${app.name:Unknown Application}")
private String applicationName;
@Value("${app.environment:development}")
private String environment;
@Value("${exception.notifier.email.enabled:false}")
private boolean emailEnabled;
@Value("${exception.notifier.slack.enabled:false}")
private boolean slackEnabled;
@Value("${exception.notifier.email.recipients:}")
private String emailRecipients;
private ExceptionCaptureService exceptionCaptureService;
@Bean
public ExceptionCaptureService exceptionCaptureService() {
exceptionCaptureService = new ExceptionCaptureService();
// Always add logging handler
exceptionCaptureService.addHandler(new LoggingHandler("Logger"));
// Add email handler if enabled
if (emailEnabled && !emailRecipients.isEmpty()) {
EmailNotificationHandler emailHandler = createEmailHandler();
exceptionCaptureService.addHandler(emailHandler);
}
// Add Slack handler if enabled
if (slackEnabled) {
SlackNotificationHandler slackHandler = createSlackHandler();
exceptionCaptureService.addHandler(slackHandler);
}
// Suppress certain exceptions
exceptionCaptureService.suppressException("org.springframework.dao.EmptyResultDataAccessException");
exceptionCaptureService.suppressException("org.springframework.web.client.HttpClientErrorException$NotFound");
return exceptionCaptureService;
}
private EmailNotificationHandler createEmailHandler() {
List<String> recipients = Arrays.asList(emailRecipients.split(","));
return new EmailNotificationHandler(
"EmailNotifier",
System.getProperty("smtp.host", "localhost"),
Integer.parseInt(System.getProperty("smtp.port", "587")),
System.getProperty("smtp.username", ""),
System.getProperty("smtp.password", ""),
Boolean.parseBoolean(System.getProperty("smtp.ssl", "false")),
System.getProperty("smtp.from", "[email protected]"),
recipients
);
}
private SlackNotificationHandler createSlackHandler() {
return new SlackNotificationHandler(
"SlackNotifier",
System.getProperty("slack.webhook.url", ""),
System.getProperty("slack.channel", "#exceptions"),
System.getProperty("slack.username", "Exception Bot"),
System.getProperty("slack.icon", ":warning:")
);
}
@PreDestroy
public void cleanup() {
if (exceptionCaptureService != null) {
exceptionCaptureService.shutdown();
}
}
}

Spring Boot Exception Handler

package com.exceptionnotifier;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
private final ExceptionCaptureService exceptionCaptureService;
public GlobalExceptionHandler(ExceptionCaptureService exceptionCaptureService) {
this.exceptionCaptureService = exceptionCaptureService;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleAllExceptions(Exception ex, WebRequest request) {
// Create context with request information
Map<String, Object> context = new HashMap<>();
context.put("requestPath", request.getContextPath());
context.put("userPrincipal", request.getUserPrincipal() != null ? 
request.getUserPrincipal().getName() : "anonymous");
context.put("remoteAddress", request.getRemoteUser());
// Capture the exception
exceptionCaptureService.captureException(ex, 
"Unhandled exception in web request", context, "WebApplication");
// Return user-friendly response
Map<String, Object> response = new HashMap<>();
response.put("error", "An unexpected error occurred");
response.put("message", "Please try again later");
response.put("timestamp", System.currentTimeMillis());
return response;
}
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleRuntimeExceptions(RuntimeException ex, WebRequest request) {
Map<String, Object> context = new HashMap<>();
context.put("requestPath", request.getContextPath());
exceptionCaptureService.captureException(ex, 
"Business logic exception", context, "WebApplication");
Map<String, Object> response = new HashMap<>();
response.put("error", "Invalid request");
response.put("message", ex.getMessage());
response.put("timestamp", System.currentTimeMillis());
return response;
}
}

Aspect-Oriented Exception Capture

package com.exceptionnotifier;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class ExceptionCaptureAspect {
@Autowired
private ExceptionCaptureService exceptionCaptureService;
@Pointcut("execution(* com.yourcompany..service.*.*(..))")
public void serviceMethods() {}
@Pointcut("execution(* com.yourcompany..repository.*.*(..))")
public void repositoryMethods() {}
@Around("serviceMethods() || repositoryMethods()")
public Object captureServiceExceptions(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (Exception ex) {
// Capture exception with method context
Map<String, Object> context = new HashMap<>();
context.put("method", joinPoint.getSignature().toShortString());
context.put("arguments", Arrays.toString(joinPoint.getArgs()));
context.put("targetClass", joinPoint.getTarget().getClass().getSimpleName());
exceptionCaptureService.captureException(ex, 
"Exception in business method", context, "BusinessLayer");
throw ex; // Re-throw the exception
}
}
@Around("@annotation(MonitorPerformance)")
public Object capturePerformanceExceptions(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return joinPoint.proceed();
} catch (Exception ex) {
long duration = System.currentTimeMillis() - startTime;
Map<String, Object> context = new HashMap<>();
context.put("method", joinPoint.getSignature().toShortString());
context.put("durationMs", duration);
context.put("performanceThresholdExceeded", duration > 1000);
exceptionCaptureService.captureException(ex, 
"Performance-related exception", context, "PerformanceMonitor");
throw ex;
}
}
}
// Custom annotation for performance monitoring
@interface MonitorPerformance {
}

Usage Examples

Basic Usage

public class ApplicationService {
private final ExceptionCaptureService exceptionCaptureService;
public ApplicationService(ExceptionCaptureService exceptionCaptureService) {
this.exceptionCaptureService = exceptionCaptureService;
}
public void processData(String data) {
try {
// Business logic that might throw exceptions
validateData(data);
transformData(data);
saveData(data);
} catch (ValidationException e) {
// Business exception - just capture for logging
Map<String, Object> context = new HashMap<>();
context.put("data", data);
context.put("validationRule", e.getRule());
exceptionCaptureService.captureException(e, 
"Data validation failed", context, "DataProcessor");
throw e;
} catch (Exception e) {
// Unexpected exception - notify through all channels
Map<String, Object> context = new HashMap<>();
context.put("data", data);
context.put("dataLength", data.length());
exceptionCaptureService.captureException(e, 
"Unexpected error processing data", context, "DataProcessor");
throw new RuntimeException("Processing failed", e);
}
}
public void batchProcess(List<String> dataList) {
for (String data : dataList) {
try {
processData(data);
} catch (Exception e) {
// Continue processing other items
System.err.println("Failed to process item: " + data);
}
}
}
}

Web Application Usage

@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final ExceptionCaptureService exceptionCaptureService;
public UserController(UserService userService, 
ExceptionCaptureService exceptionCaptureService) {
this.userService = userService;
this.exceptionCaptureService = exceptionCaptureService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable String id, 
HttpServletRequest request) {
try {
User user = userService.findUserById(id);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
// Expected exception - just log
Map<String, Object> context = new HashMap<>();
context.put("userId", id);
context.put("clientIp", request.getRemoteAddr());
exceptionCaptureService.captureException(e, 
"User not found", context, "UserAPI");
return ResponseEntity.notFound().build();
} catch (Exception e) {
// Unexpected exception - notify
Map<String, Object> context = new HashMap<>();
context.put("userId", id);
context.put("endpoint", "/api/users/" + id);
context.put("httpMethod", "GET");
context.put("clientIp", request.getRemoteAddr());
exceptionCaptureService.captureException(e, 
"Unexpected error fetching user", context, "UserAPI");
return ResponseEntity.status(500).build();
}
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user, 
HttpServletRequest request) {
try {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(createdUser);
} catch (DuplicateUserException e) {
Map<String, Object> context = new HashMap<>();
context.put("userEmail", user.getEmail());
context.put("clientIp", request.getRemoteAddr());
exceptionCaptureService.captureException(e, 
"Duplicate user creation attempt", context, "UserAPI");
return ResponseEntity.badRequest().build();
}
}
}

Key Features

  1. Multi-channel Notifications: Email, Slack, logging, and extensible for more
  2. Intelligent Grouping: Deduplicates similar exceptions to avoid notification spam
  3. Rate Limiting: Prevents flooding from recurring exceptions
  4. Context Enrichment: Adds relevant context to each exception
  5. Severity-based Routing: Different handlers for different severity levels
  6. Async Processing: Non-blocking exception processing
  7. Spring Integration: Easy integration with Spring Boot applications
  8. Customizable: Extensible handlers and configuration options

This Exception Notification System provides comprehensive exception management with intelligent notification routing and minimal performance impact.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper