Article
Exceptionless is an open-source error reporting and monitoring platform that provides real-time error reporting, aggregation, and notifications. It helps developers track, analyze, and resolve errors in their applications. This guide covers everything from basic setup to advanced error handling patterns in Java.
Why Exceptionless?
- Real-time Error Reporting: Immediate notification of exceptions
- Error Aggregation: Groups similar errors to reduce noise
- Rich Context: Includes environment data, user information, and custom properties
- Open Source: Self-hosted or cloud-based options
- Powerful Search: Find errors using advanced filtering
- Notifications: Email, Slack, Webhook integrations
- Trend Analysis: Track error frequency and impact over time
Project Setup and Dependencies
Maven Dependencies:
<properties>
<exceptionless.version>6.0.4</exceptionless.version>
<spring.boot.version>3.1.0</spring.boot.version>
<logback.version>1.4.11</logback.version>
</properties>
<dependencies>
<!-- Exceptionless Java Client -->
<dependency>
<groupId>com.exceptionless</groupId>
<artifactId>exceptionless-client</artifactId>
<version>${exceptionless.version}</version>
</dependency>
<!-- Exceptionless Logback Appender -->
<dependency>
<groupId>com.exceptionless</groupId>
<artifactId>exceptionless-logback</artifactId>
<version>${exceptionless.version}</version>
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- For async operations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-async</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
1. Basic Exceptionless Configuration
Exceptionless Configuration Service:
package com.example.exceptionless.config;
import com.exceptionless.ExceptionlessClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Configuration
public class ExceptionlessConfig {
@Value("${exceptionless.apiKey:}")
private String apiKey;
@Value("${exceptionless.serverUrl:https://collector.exceptionless.com}")
private String serverUrl;
@Value("${exceptionless.enabled:true}")
private boolean enabled;
@Bean
public ExceptionlessClient exceptionlessClient() {
ExceptionlessClient client = ExceptionlessClient.fromConfig(apiKey);
if (!enabled) {
client.config().setEnabled(false);
return client;
}
// Configure client
client.config()
.setServerUrl(serverUrl)
.setEnabled(true)
.useDebugLogger();
// Set default tags
client.config().getDefaultTags().add("java");
client.config().getDefaultTags().add("spring-boot");
client.config().getDefaultTags().add(getEnvironment());
// Set default data
client.config().getDefaultData().put("Application", "Order Service");
client.config().getDefaultData().put("Version", "1.0.0");
client.config().getDefaultData().put("Environment", getEnvironment());
return client;
}
@PostConstruct
public void initialize() {
if (enabled) {
ExceptionlessClient client = exceptionlessClient();
client.submitLog("Application starting...", "INFO");
System.out.println("Exceptionless initialized for environment: " + getEnvironment());
}
}
@PreDestroy
public void shutdown() {
if (enabled) {
ExceptionlessClient client = exceptionlessClient();
client.submitLog("Application shutting down...", "INFO");
client.config().setEnabled(false);
}
}
private String getEnvironment() {
String env = System.getenv("APP_ENVIRONMENT");
if (env == null || env.isEmpty()) {
env = System.getProperty("app.environment", "development");
}
return env;
}
}
2. Exceptionless Service Layer
Core Exceptionless Service:
package com.example.exceptionless.service;
import com.exceptionless.ExceptionlessClient;
import com.exceptionless.models.Event;
import com.exceptionless.models.EventType;
import com.exceptionless.models.UserInfo;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class ExceptionlessService {
private final ExceptionlessClient client;
public ExceptionlessService(ExceptionlessClient client) {
this.client = client;
}
// Submit exception with context
public void submitException(Throwable exception, Map<String, Object> data,
String userIdentity, String userDescription) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createException(exception)
.setType(EventType.ERROR);
// Add custom data
if (data != null) {
data.forEach(builder::setProperty);
}
// Set user information
if (userIdentity != null) {
builder.setUserIdentity(userIdentity, userDescription);
}
// Submit asynchronously
CompletableFuture.runAsync(builder::submit);
}
// Submit log messages
public void submitLog(String message, String level, Map<String, Object> data) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.submitLog(message, level);
if (data != null) {
data.forEach(builder::setProperty);
}
CompletableFuture.runAsync(builder::submit);
}
// Submit feature usage
public void submitFeatureUsage(String feature, Map<String, Object> data) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createFeatureUsage(feature);
if (data != null) {
data.forEach(builder::setProperty);
}
CompletableFuture.runAsync(builder::submit);
}
// Submit custom event
public void submitEvent(String type, String source, Map<String, Object> data) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createEvent()
.setType(type)
.setSource(source);
if (data != null) {
data.forEach(builder::setProperty);
}
CompletableFuture.runAsync(builder::submit);
}
// Submit 404 not found
public void submitNotFound(String resource, String userIdentity) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createNotFound(resource);
if (userIdentity != null) {
builder.setUserIdentity(userIdentity);
}
CompletableFuture.runAsync(builder::submit);
}
// Submit session start
public void submitSessionStart(String userIdentity, Map<String, Object> data) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createSessionStart();
if (userIdentity != null) {
builder.setUserIdentity(userIdentity);
}
if (data != null) {
data.forEach(builder::setProperty);
}
CompletableFuture.runAsync(builder::submit);
}
// Submit session end
public void submitSessionEnd(String userIdentity) {
if (!client.config().isEnabled()) {
return;
}
EventBuilder builder = client.createSessionEnd();
if (userIdentity != null) {
builder.setUserIdentity(userIdentity);
}
CompletableFuture.runAsync(builder::submit);
}
// Business-specific error tracking
public void submitBusinessError(String errorCode, String message,
String userId, Map<String, Object> context) {
Map<String, Object> data = new HashMap<>();
data.put("ErrorCode", errorCode);
data.put("BusinessError", true);
if (context != null) {
data.putAll(context);
}
submitEvent("business-error", "business", data);
// Also log as error
submitLog(message, "ERROR", data);
}
// Performance monitoring
public void submitPerformanceMetric(String operation, long durationMs,
boolean success, Map<String, Object> tags) {
Map<String, Object> data = new HashMap<>();
data.put("Operation", operation);
data.put("DurationMs", durationMs);
data.put("Success", success);
if (tags != null) {
data.putAll(tags);
}
submitEvent("performance", "monitoring", data);
}
// Security event tracking
public void submitSecurityEvent(String eventType, String userIdentity,
String resource, String action) {
Map<String, Object> data = new HashMap<>();
data.put("SecurityEventType", eventType);
data.put("Resource", resource);
data.put("Action", action);
submitEvent("security", "security", data);
// Also include in logs
submitLog(String.format("Security event: %s - %s on %s", eventType, action, resource),
"WARN", data);
}
}
3. Global Exception Handling
Global Exception Handler:
package com.example.exceptionless.handler;
import com.example.exceptionless.service.ExceptionlessService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
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 static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final ExceptionlessService exceptionlessService;
public GlobalExceptionHandler(ExceptionlessService exceptionlessService) {
this.exceptionlessService = exceptionlessService;
}
// Handle all uncaught exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAllExceptions(
Exception ex, WebRequest request) {
logger.error("Unhandled exception occurred", ex);
// Prepare context data
Map<String, Object> context = new HashMap<>();
context.put("RequestPath", request.getDescription(false));
context.put("RemoteAddress", getClientIp(request));
context.put("UserAgent", request.getHeader("User-Agent"));
// Submit to Exceptionless
exceptionlessService.submitException(ex, context, getCurrentUser(),
"Unhandled exception in request");
// Return error response
Map<String, Object> response = new HashMap<>();
response.put("error", "Internal Server Error");
response.put("message", "An unexpected error occurred");
response.put("timestamp", System.currentTimeMillis());
response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
// Handle business exceptions
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusinessException(
BusinessException ex, WebRequest request) {
logger.warn("Business exception: {}", ex.getMessage());
Map<String, Object> context = new HashMap<>();
context.put("ErrorCode", ex.getErrorCode());
context.put("BusinessContext", ex.getContext());
context.put("RequestPath", request.getDescription(false));
exceptionlessService.submitBusinessError(
ex.getErrorCode(),
ex.getMessage(),
getCurrentUser(),
context
);
Map<String, Object> response = new HashMap<>();
response.put("error", "Business Error");
response.put("message", ex.getMessage());
response.put("errorCode", ex.getErrorCode());
response.put("timestamp", System.currentTimeMillis());
response.put("status", HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
// Handle validation exceptions
@ExceptionHandler(ValidationException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(
ValidationException ex, WebRequest request) {
Map<String, Object> context = new HashMap<>();
context.put("ValidationErrors", ex.getValidationErrors());
context.put("RequestPath", request.getDescription(false));
exceptionlessService.submitException(ex, context, getCurrentUser(),
"Validation failed");
Map<String, Object> response = new HashMap<>();
response.put("error", "Validation Failed");
response.put("message", ex.getMessage());
response.put("validationErrors", ex.getValidationErrors());
response.put("timestamp", System.currentTimeMillis());
response.put("status", HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
// Handle resource not found
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleResourceNotFoundException(
ResourceNotFoundException ex, WebRequest request) {
Map<String, Object> context = new HashMap<>();
context.put("ResourceType", ex.getResourceType());
context.put("ResourceId", ex.getResourceId());
context.put("RequestPath", request.getDescription(false));
exceptionlessService.submitNotFound(
String.format("%s with id %s", ex.getResourceType(), ex.getResourceId()),
getCurrentUser()
);
Map<String, Object> response = new HashMap<>();
response.put("error", "Resource Not Found");
response.put("message", ex.getMessage());
response.put("resourceType", ex.getResourceType());
response.put("resourceId", ex.getResourceId());
response.put("timestamp", System.currentTimeMillis());
response.put("status", HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
// Handle access denied
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Map<String, Object>> handleAccessDeniedException(
AccessDeniedException ex, WebRequest request) {
Map<String, Object> context = new HashMap<>();
context.put("RequiredPermission", ex.getRequiredPermission());
context.put("Resource", ex.getResource());
context.put("RequestPath", request.getDescription(false));
exceptionlessService.submitSecurityEvent(
"ACCESS_DENIED",
getCurrentUser(),
ex.getResource(),
"ACCESS"
);
Map<String, Object> response = new HashMap<>();
response.put("error", "Access Denied");
response.put("message", ex.getMessage());
response.put("timestamp", System.currentTimeMillis());
response.put("status", HttpStatus.FORBIDDEN.value());
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
}
private String getClientIp(WebRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return "unknown";
}
private String getCurrentUser() {
// Implement based on your authentication system
// For example, from Spring Security context
return "anonymous"; // Replace with actual user identity
}
}
// Custom exception classes
class BusinessException extends RuntimeException {
private final String errorCode;
private final Map<String, Object> context;
public BusinessException(String message, String errorCode, Map<String, Object> context) {
super(message);
this.errorCode = errorCode;
this.context = context != null ? new HashMap<>(context) : new HashMap<>();
}
public String getErrorCode() { return errorCode; }
public Map<String, Object> getContext() { return context; }
}
class ValidationException extends RuntimeException {
private final Map<String, String> validationErrors;
public ValidationException(String message, Map<String, String> validationErrors) {
super(message);
this.validationErrors = validationErrors != null ?
new HashMap<>(validationErrors) : new HashMap<>();
}
public Map<String, String> getValidationErrors() { return validationErrors; }
}
class ResourceNotFoundException extends RuntimeException {
private final String resourceType;
private final String resourceId;
public ResourceNotFoundException(String resourceType, String resourceId) {
super(String.format("%s with id %s not found", resourceType, resourceId));
this.resourceType = resourceType;
this.resourceId = resourceId;
}
public String getResourceType() { return resourceType; }
public String getResourceId() { return resourceId; }
}
class AccessDeniedException extends RuntimeException {
private final String requiredPermission;
private final String resource;
public AccessDeniedException(String requiredPermission, String resource) {
super(String.format("Access denied. Required permission: %s for resource: %s",
requiredPermission, resource));
this.requiredPermission = requiredPermission;
this.resource = resource;
}
public String getRequiredPermission() { return requiredPermission; }
public String getResource() { return resource; }
}
4. Logback Integration
logback-spring.xml Configuration:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Exceptionless Appender -->
<appender name="EXCEPTIONLESS" class="com.exceptionless.logback.ExceptionlessAppender">
<apiKey>${EXCEPTIONLESS_API_KEY}</apiKey>
<serverUrl>${EXCEPTIONLESS_SERVER_URL:-https://collector.exceptionless.com}</serverUrl>
<!-- Additional configuration -->
<tags>java,spring-boot,${APP_ENVIRONMENT:-development}</tags>
<!-- Default data -->
<data>
<Application>Order Service</Application>
<Version>1.0.0</Version>
<Environment>${APP_ENVIRONMENT:-development}</Environment>
</data>
<!-- Only send ERROR level by default -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Async Appender for Exceptionless -->
<appender name="ASYNC_EXCEPTIONLESS" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="EXCEPTIONLESS" />
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
</appender>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_EXCEPTIONLESS" />
</root>
<!-- Application-specific loggers -->
<logger name="com.example.exceptionless" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_EXCEPTIONLESS" />
</logger>
<!-- Reduce noise from framework logs -->
<logger name="org.springframework" level="WARN" />
<logger name="org.hibernate" level="WARN" />
</configuration>
5. Spring Boot Integration
Exceptionless Auto-Configuration:
package com.example.exceptionless.config;
import com.example.exceptionless.service.ExceptionlessService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
@EnableConfigurationProperties(ExceptionlessProperties.class)
public class ExceptionlessAutoConfig {
@Bean
public ExceptionlessService exceptionlessService(com.exceptionless.ExceptionlessClient client) {
return new ExceptionlessService(client);
}
@Bean
public GlobalExceptionHandler globalExceptionHandler(ExceptionlessService exceptionlessService) {
return new GlobalExceptionHandler(exceptionlessService);
}
}
@ConfigurationProperties(prefix = "exceptionless")
class ExceptionlessProperties {
private String apiKey;
private String serverUrl = "https://collector.exceptionless.com";
private boolean enabled = true;
private int queueSize = 1000;
private int batchSize = 50;
private long flushInterval = 30000; // 30 seconds
// Getters and setters
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getServerUrl() { return serverUrl; }
public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getQueueSize() { return queueSize; }
public void setQueueSize(int queueSize) { this.queueSize = queueSize; }
public int getBatchSize() { return batchSize; }
public void setBatchSize(int batchSize) { this.batchSize = batchSize; }
public long getFlushInterval() { return flushInterval; }
public void setFlushInterval(long flushInterval) { this.flushInterval = flushInterval; }
}
6. Business Service with Exceptionless Integration
Order Service Example:
package com.example.exceptionless.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
private final ExceptionlessService exceptionlessService;
public OrderService(ExceptionlessService exceptionlessService) {
this.exceptionlessService = exceptionlessService;
}
public Order createOrder(CreateOrderRequest request) {
long startTime = System.currentTimeMillis();
try {
logger.info("Creating order for customer: {}", request.getCustomerId());
// Track feature usage
exceptionlessService.submitFeatureUsage("CreateOrder", Map.of(
"customerId", request.getCustomerId(),
"itemCount", request.getItems().size(),
"totalAmount", request.getTotalAmount()
));
// Validate business rules
validateOrder(request);
// Process order
Order order = processOrder(request);
long duration = System.currentTimeMillis() - startTime;
// Track performance
exceptionlessService.submitPerformanceMetric("CreateOrder", duration, true, Map.of(
"orderId", order.getId(),
"customerId", request.getCustomerId()
));
logger.info("Order created successfully: {}", order.getId());
return order;
} catch (BusinessException e) {
long duration = System.currentTimeMillis() - startTime;
// Track failed performance
exceptionlessService.submitPerformanceMetric("CreateOrder", duration, false, Map.of(
"customerId", request.getCustomerId(),
"error", e.getErrorCode()
));
// Re-throw for global handler
throw e;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
// Track unexpected errors
Map<String, Object> context = new HashMap<>();
context.put("customerId", request.getCustomerId());
context.put("operation", "CreateOrder");
context.put("durationMs", duration);
exceptionlessService.submitException(e, context, request.getCustomerId(),
"Unexpected error creating order");
throw new RuntimeException("Failed to create order", e);
}
}
public Order getOrder(String orderId) {
try {
logger.debug("Retrieving order: {}", orderId);
Order order = findOrderById(orderId);
if (order == null) {
throw new ResourceNotFoundException("Order", orderId);
}
return order;
} catch (ResourceNotFoundException e) {
// This will be handled by global exception handler
throw e;
} catch (Exception e) {
Map<String, Object> context = new HashMap<>();
context.put("orderId", orderId);
context.put("operation", "GetOrder");
exceptionlessService.submitException(e, context, getCurrentUser(),
"Error retrieving order");
throw new RuntimeException("Failed to retrieve order", e);
}
}
public void processPayment(String orderId, Payment payment) {
long startTime = System.currentTimeMillis();
try {
logger.info("Processing payment for order: {}", orderId);
// Validate payment
if (!isValidPayment(payment)) {
throw new BusinessException("Invalid payment", "INVALID_PAYMENT", Map.of(
"orderId", orderId,
"paymentMethod", payment.getMethod(),
"paymentAmount", payment.getAmount()
));
}
// Process payment
boolean success = executePayment(payment);
if (!success) {
throw new BusinessException("Payment processing failed", "PAYMENT_FAILED", Map.of(
"orderId", orderId,
"paymentGateway", payment.getGateway(),
"errorCode", payment.getErrorCode()
));
}
long duration = System.currentTimeMillis() - startTime;
// Track successful payment
exceptionlessService.submitPerformanceMetric("ProcessPayment", duration, true, Map.of(
"orderId", orderId,
"paymentMethod", payment.getMethod(),
"amount", payment.getAmount()
));
logger.info("Payment processed successfully for order: {}", orderId);
} catch (BusinessException e) {
long duration = System.currentTimeMillis() - startTime;
// Track failed payment performance
exceptionlessService.submitPerformanceMetric("ProcessPayment", duration, false, Map.of(
"orderId", orderId,
"error", e.getErrorCode()
));
throw e;
}
}
private void validateOrder(CreateOrderRequest request) {
if (request.getItems().isEmpty()) {
throw new BusinessException("Order must contain at least one item",
"EMPTY_ORDER", Map.of(
"customerId", request.getCustomerId()
));
}
if (request.getTotalAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
throw new BusinessException("Order total must be greater than zero",
"INVALID_TOTAL", Map.of(
"customerId", request.getCustomerId(),
"totalAmount", request.getTotalAmount()
));
}
// Check inventory
for (OrderItem item : request.getItems()) {
if (!isInStock(item.getProductId(), item.getQuantity())) {
throw new BusinessException("Product out of stock", "OUT_OF_STOCK", Map.of(
"productId", item.getProductId(),
"requestedQuantity", item.getQuantity()
));
}
}
}
// Mock methods - implement with actual logic
private Order processOrder(CreateOrderRequest request) {
// Implementation details
return new Order(java.util.UUID.randomUUID().toString(),
request.getCustomerId(),
request.getItems(),
request.getTotalAmount());
}
private Order findOrderById(String orderId) {
// Implementation details
return null; // Simulate not found
}
private boolean isValidPayment(Payment payment) {
return payment != null && payment.getAmount().compareTo(java.math.BigDecimal.ZERO) > 0;
}
private boolean executePayment(Payment payment) {
// Implementation details
return Math.random() > 0.1; // 90% success rate for demo
}
private boolean isInStock(String productId, int quantity) {
// Implementation details
return Math.random() > 0.05; // 95% in stock for demo
}
private String getCurrentUser() {
// Implement based on your authentication system
return "user123";
}
}
// Domain classes
class Order {
private final String id;
private final String customerId;
private final java.util.List<OrderItem> items;
private final java.math.BigDecimal totalAmount;
public Order(String id, String customerId, java.util.List<OrderItem> items,
java.math.BigDecimal totalAmount) {
this.id = id;
this.customerId = customerId;
this.items = items;
this.totalAmount = totalAmount;
}
// Getters
public String getId() { return id; }
public String getCustomerId() { return customerId; }
public java.util.List<OrderItem> getItems() { return items; }
public java.math.BigDecimal getTotalAmount() { return totalAmount; }
}
class CreateOrderRequest {
private String customerId;
private java.util.List<OrderItem> items;
private java.math.BigDecimal totalAmount;
// Getters and setters
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public java.util.List<OrderItem> getItems() { return items; }
public void setItems(java.util.List<OrderItem> items) { this.items = items; }
public java.math.BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(java.math.BigDecimal totalAmount) { this.totalAmount = totalAmount; }
}
class OrderItem {
private String productId;
private int quantity;
private java.math.BigDecimal price;
// Getters and setters
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public java.math.BigDecimal getPrice() { return price; }
public void setPrice(java.math.BigDecimal price) { this.price = price; }
}
class Payment {
private String method;
private java.math.BigDecimal amount;
private String gateway;
private String errorCode;
// Getters and setters
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public java.math.BigDecimal getAmount() { return amount; }
public void setAmount(java.math.BigDecimal amount) { this.amount = amount; }
public String getGateway() { return gateway; }
public void setGateway(String gateway) { this.gateway = gateway; }
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
}
7. REST Controller with Exceptionless
Order Controller:
package com.example.exceptionless.controller;
import com.example.exceptionless.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Map<String, Object>> createOrder(@RequestBody CreateOrderRequest request) {
logger.info("Received order creation request for customer: {}", request.getCustomerId());
try {
Order order = orderService.createOrder(request);
Map<String, Object> response = Map.of(
"success", true,
"orderId", order.getId(),
"message", "Order created successfully"
);
return ResponseEntity.ok(response);
} catch (Exception e) {
// Exception will be handled by GlobalExceptionHandler
throw e;
}
}
@GetMapping("/{orderId}")
public ResponseEntity<Map<String, Object>> getOrder(@PathVariable String orderId) {
logger.debug("Retrieving order: {}", orderId);
try {
Order order = orderService.getOrder(orderId);
Map<String, Object> response = Map.of(
"success", true,
"order", order
);
return ResponseEntity.ok(response);
} catch (Exception e) {
throw e;
}
}
@PostMapping("/{orderId}/payments")
public ResponseEntity<Map<String, Object>> processPayment(
@PathVariable String orderId,
@RequestBody Payment payment) {
logger.info("Processing payment for order: {}", orderId);
try {
orderService.processPayment(orderId, payment);
Map<String, Object> response = Map.of(
"success", true,
"message", "Payment processed successfully"
);
return ResponseEntity.ok(response);
} catch (Exception e) {
throw e;
}
}
}
8. Configuration Files
application.yml:
exceptionless:
api-key: ${EXCEPTIONLESS_API_KEY:}
server-url: ${EXCEPTIONLESS_SERVER_URL:https://collector.exceptionless.com}
enabled: ${EXCEPTIONLESS_ENABLED:true}
queue-size: 1000
batch-size: 50
flush-interval: 30000
logging:
level:
com.example.exceptionless: INFO
com.exceptionless: WARN
spring:
application:
name: order-service
server:
port: 8080
app:
environment: ${APP_ENVIRONMENT:development}
version: 1.0.0
9. Docker Deployment
Dockerfile:
FROM openjdk:17-jre-slim # Install curl for health checks RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* # Copy application COPY target/order-service.jar /app/ # Create non-root user RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser WORKDIR /app # Set environment variables ENV EXCEPTIONLESS_API_KEY="" ENV EXCEPTIONLESS_SERVER_URL="https://collector.exceptionless.com" ENV EXCEPTIONLESS_ENABLED="true" ENV APP_ENVIRONMENT="production" ENV JAVA_OPTS="-Xms512m -Xmx1024m" EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 CMD ["sh", "-c", "java $JAVA_OPTS -jar order-service.jar"]
docker-compose.yml:
version: '3.8'
services:
order-service:
build: .
ports:
- "8080:8080"
environment:
- EXCEPTIONLESS_API_KEY=${EXCEPTIONLESS_API_KEY}
- EXCEPTIONLESS_SERVER_URL=${EXCEPTIONLESS_SERVER_URL}
- EXCEPTIONLESS_ENABLED=true
- APP_ENVIRONMENT=production
- JAVA_OPTS=-Xms512m -Xmx1024m
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
10. Best Practices
Error Context Enrichment:
@Component
public class ExceptionContextEnricher {
public Map<String, Object> enrichContext(WebRequest request, String userIdentity) {
Map<String, Object> context = new HashMap<>();
// Request information
context.put("RequestPath", request.getDescription(false));
context.put("RemoteAddress", getClientIp(request));
context.put("UserAgent", request.getHeader("User-Agent"));
context.put("Referer", request.getHeader("Referer"));
// Application context
context.put("ApplicationVersion", "1.0.0");
context.put("Environment", getEnvironment());
context.put("InstanceId", getInstanceId());
// User context
if (userIdentity != null) {
context.put("UserIdentity", userIdentity);
}
// System context
context.put("AvailableProcessors", Runtime.getRuntime().availableProcessors());
context.put("FreeMemory", Runtime.getRuntime().freeMemory());
context.put("TotalMemory", Runtime.getRuntime().totalMemory());
return context;
}
private String getClientIp(WebRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return "unknown";
}
private String getEnvironment() {
return System.getenv().getOrDefault("APP_ENVIRONMENT", "development");
}
private String getInstanceId() {
return System.getenv().getOrDefault("HOSTNAME", "unknown");
}
}
Performance Monitoring:
@Aspect
@Component
public class PerformanceMonitoringAspect {
private final ExceptionlessService exceptionlessService;
public PerformanceMonitoringAspect(ExceptionlessService exceptionlessService) {
this.exceptionlessService = exceptionlessService;
}
@Around("@annotation(MonitorPerformance)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
// Log performance for successful execution
exceptionlessService.submitPerformanceMetric(methodName, duration, true, Map.of(
"class", joinPoint.getSignature().getDeclaringTypeName()
));
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
// Log performance for failed execution
exceptionlessService.submitPerformanceMetric(methodName, duration, false, Map.of(
"class", joinPoint.getSignature().getDeclaringTypeName(),
"error", e.getClass().getSimpleName()
));
throw e;
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorPerformance {
String value() default "";
}
Conclusion
Exceptionless provides a powerful, open-source error monitoring solution for Java applications. Key benefits include:
- Real-time Error Reporting: Immediate notification of issues
- Rich Context: Comprehensive error details for faster debugging
- Error Aggregation: Reduces noise by grouping similar errors
- Flexible Deployment: Self-hosted or cloud-based options
- Powerful Search: Advanced filtering and query capabilities
- Integration: Works with logging frameworks and custom code
By implementing the patterns shown above, you can achieve comprehensive error monitoring, performance tracking, and business event logging that will help you maintain high-quality, reliable Java applications.