Error Tracking with Sentry in Java

Overview

Sentry is a powerful error tracking and performance monitoring platform that helps developers identify, triage, and resolve issues in real-time. Here's a comprehensive guide to implementing Sentry in Java applications.

Setup and Configuration

1. Maven Dependency

<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry</artifactId>
<version>6.27.0</version>
</dependency>
<!-- Spring Boot Starter (if using Spring Boot) -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter</artifactId>
<version>6.27.0</version>
</dependency>
<!-- Logback integration -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>6.27.0</version>
</dependency>
<!-- HTTP client integration -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-apache-http-client-5</artifactId>
<version>6.27.0</version>
</dependency>
</dependencies>

2. Basic Configuration

// Application.java
import io.sentry.Sentry;
public class Application {
public static void main(String[] args) {
// Initialize Sentry
Sentry.init(options -> {
options.setDsn("https://[email protected]/project-id");
options.setEnvironment("production");
options.setRelease("[email protected]");
options.setTracesSampleRate(0.2); // Sample 20% of transactions
options.setProfilesSampleRate(0.1); // Sample 10% of profiles
});
try {
new Application().run();
} catch (Exception e) {
Sentry.captureException(e);
} finally {
Sentry.close();
}
}
public void run() {
// Application logic
System.out.println("Application started with Sentry monitoring");
}
}

3. Spring Boot Configuration

// application.yml
sentry:
dsn: https://[email protected]/project-id
environment: ${SENTRY_ENVIRONMENT:development}
release: my-app@${APP_VERSION:1.0.0}
traces-sample-rate: 0.1
profiles-sample-rate: 0.05
send-default-pii: true
logging:
enabled: true
minimum-event-level: WARN
minimum-breadcrumb-level: INFO
# application.properties alternative
sentry.dsn=https://[email protected]/project-id
sentry.environment=production
[email protected]
sentry.traces-sample-rate=0.1
sentry.profiles-sample-rate=0.05
// Spring Boot configuration class
@Configuration
@EnableSentry
public class SentryConfig {
@Bean
public SentryOptionsConfiguration sentryOptionsConfiguration() {
return options -> {
options.setEnableTracing(true);
options.setTracesSampleRate(0.2);
options.setBeforeSend((event, hint) -> {
// Filter out sensitive data
if (event.getRequest() != null) {
event.getRequest().getHeaders().remove("Authorization");
}
return event;
});
};
}
}

Basic Error Tracking

1. Manual Exception Capture

import io.sentry.Sentry;
import io.sentry.protocol.User;
public class OrderService {
public void processOrder(Order order) {
// Set user context for better debugging
User user = new User();
user.setId(order.getUserId());
user.setEmail(order.getUserEmail());
user.setUsername(order.getUsername());
Sentry.setUser(user);
// Add custom tags
Sentry.setTag("order_type", order.getType());
Sentry.setTag("payment_method", order.getPaymentMethod());
try {
validateOrder(order);
processPayment(order);
updateInventory(order);
sendConfirmation(order);
// Record successful operation as breadcrumb
Sentry.addBreadcrumb(Breadcrumb.info(
"Order processed successfully",
"order_processing",
Map.of("order_id", order.getId(), "amount", order.getAmount())
));
} catch (PaymentException e) {
// Capture payment-specific errors with context
Sentry.configureScope(scope -> {
scope.setContexts("payment", Map.of(
"gateway", order.getPaymentGateway(),
"transaction_id", order.getTransactionId(),
"amount", order.getAmount()
));
});
Sentry.captureException(e);
throw new BusinessException("Payment processing failed", e);
} catch (InventoryException e) {
// Capture inventory errors with custom fingerprint
SentryEvent event = new SentryEvent(e);
event.setFingerprints(Arrays.asList(
"inventory-error",
order.getProductId(),
"{{ default }}"
));
Sentry.captureEvent(event);
throw new BusinessException("Inventory update failed", e);
} catch (Exception e) {
// Capture all other exceptions
Sentry.captureException(e, scope -> {
scope.setLevel(SentryLevel.ERROR);
scope.setExtra("order_data", order.toString());
});
throw new BusinessException("Order processing failed", e);
} finally {
// Clear user context
Sentry.setUser(null);
}
}
private void validateOrder(Order order) {
if (order.getAmount() <= 0) {
throw new ValidationException("Invalid order amount: " + order.getAmount());
}
// Add breadcrumb for validation
Sentry.addBreadcrumb(Breadcrumb.info(
"Order validated",
"validation",
Map.of("order_id", order.getId(), "items_count", order.getItems().size())
));
}
private void processPayment(Order order) throws PaymentException {
try {
// Payment processing logic
paymentGateway.charge(order);
Sentry.addBreadcrumb(Breadcrumb.info(
"Payment processed",
"payment",
Map.of("transaction_id", order.getTransactionId(), "status", "success")
));
} catch (PaymentGatewayException e) {
Sentry.addBreadcrumb(Breadcrumb.error(
"Payment failed",
"payment",
Map.of("error", e.getMessage(), "gateway", order.getPaymentGateway())
));
throw new PaymentException("Payment gateway error", e);
}
}
}

2. Custom Exception Classes

// Custom business exceptions with Sentry integration
public class BusinessException extends RuntimeException {
private final String errorCode;
private final Map<String, Object> context;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public BusinessException(String errorCode, String message, Map<String, Object> context) {
super(message);
this.errorCode = errorCode;
this.context = context;
}
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public void captureToSentry() {
SentryEvent event = new SentryEvent(this);
event.setTag("error_code", errorCode);
event.setTag("exception_type", "business_exception");
event.setContexts("business_context", context);
// Group similar business errors together
event.setFingerprints(Arrays.asList(
"business-error",
errorCode,
"{{ default }}"
));
Sentry.captureEvent(event);
}
// Getters
public String getErrorCode() { return errorCode; }
public Map<String, Object> getContext() { return context; }
}
// Specific business exceptions
public class PaymentException extends BusinessException {
public PaymentException(String errorCode, String message) {
super(errorCode, message);
}
public PaymentException(String errorCode, String message, Map<String, Object> context) {
super(errorCode, message, context);
}
public PaymentException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
}
public class InventoryException extends BusinessException {
public InventoryException(String errorCode, String message) {
super(errorCode, message);
}
}

Advanced Features

1. Performance Monitoring

import io.sentry.Sentry;
import io.sentry.SpanStatus;
import io.sentry.ISpan;
import io.sentry.ITransaction;
public class PerformanceMonitoringService {
public CompletableFuture<OrderResult> processOrderAsync(Order order) {
// Start a transaction for the entire order processing
ITransaction transaction = Sentry.startTransaction(
"order.process", 
"task",
true // wait for children to finish
);
transaction.setData("order_id", order.getId());
transaction.setData("user_id", order.getUserId());
try {
return processOrderSteps(transaction, order)
.whenComplete((result, throwable) -> {
if (throwable != null) {
transaction.setThrowable(throwable);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
} else {
transaction.setStatus(SpanStatus.OK);
}
transaction.finish();
});
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
transaction.finish();
throw e;
}
}
private CompletableFuture<OrderResult> processOrderSteps(ITransaction transaction, Order order) {
List<CompletableFuture<Void>> steps = new ArrayList<>();
// Step 1: Validate order (child span)
steps.add(CompletableFuture.runAsync(() -> {
ISpan validateSpan = transaction.startChild("order.validate");
try {
validateOrder(order);
validateSpan.setStatus(SpanStatus.OK);
} catch (Exception e) {
validateSpan.setThrowable(e);
validateSpan.setStatus(SpanStatus.INVALID_ARGUMENT);
throw e;
} finally {
validateSpan.finish();
}
}));
// Step 2: Process payment (child span)
steps.add(CompletableFuture.runAsync(() -> {
ISpan paymentSpan = transaction.startChild("payment.process");
paymentSpan.setData("gateway", order.getPaymentGateway());
paymentSpan.setData("amount", order.getAmount());
try {
processPayment(order);
paymentSpan.setStatus(SpanStatus.OK);
} catch (Exception e) {
paymentSpan.setThrowable(e);
paymentSpan.setStatus(SpanStatus.FAILED_PRECONDITION);
throw e;
} finally {
paymentSpan.finish();
}
}));
// Step 3: Update inventory (child span)
steps.add(CompletableFuture.runAsync(() -> {
ISpan inventorySpan = transaction.startChild("inventory.update");
try {
updateInventory(order);
inventorySpan.setStatus(SpanStatus.OK);
} catch (Exception e) {
inventorySpan.setThrowable(e);
inventorySpan.setStatus(SpanStatus.RESOURCE_EXHAUSTED);
throw e;
} finally {
inventorySpan.finish();
}
}));
// Wait for all steps to complete
return CompletableFuture.allOf(steps.toArray(new CompletableFuture[0]))
.thenApply(v -> new OrderResult(order.getId(), "SUCCESS"));
}
// HTTP request monitoring
@Component
public class HttpRequestMonitor {
public <T> T executeWithMonitoring(String operation, Supplier<T> operationSupplier) {
ITransaction transaction = Sentry.getCurrentTransaction();
if (transaction == null) {
return operationSupplier.get();
}
ISpan span = transaction.startChild("http.request", operation);
try {
T result = operationSupplier.get();
span.setStatus(SpanStatus.OK);
return result;
} catch (Exception e) {
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
span.finish();
}
}
}
}

2. Database Query Monitoring

@Component
public class DatabaseMonitoringAspect {
@Around("execution(* com.example.repository.*.*(..))")
public Object monitorDatabaseOperations(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
String operation = className + "." + methodName;
ITransaction transaction = Sentry.getCurrentTransaction();
if (transaction == null) {
return joinPoint.proceed();
}
ISpan span = transaction.startChild("db.query", operation);
// Add query context
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
span.setData("method_args", Arrays.toString(args));
}
try {
Object result = joinPoint.proceed();
span.setStatus(SpanStatus.OK);
// Add result size if applicable
if (result instanceof Collection) {
span.setData("result_size", ((Collection<?>) result).size());
}
return result;
} catch (Exception e) {
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
span.finish();
}
}
}
// Repository with custom monitoring
@Repository
public class OrderRepository {
private final JdbcTemplate jdbcTemplate;
private final DatabaseMonitoringAspect monitoringAspect;
public Order findByIdWithMonitoring(String orderId) {
return monitoringAspect.executeWithMonitoring("OrderRepository.findById", () -> {
String sql = "SELECT * FROM orders WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new OrderRowMapper(), orderId);
});
}
}

3. Custom Instrumentation

@Component
public class KafkaConsumerMonitoring {
@KafkaListener(topics = "orders")
public void consumeOrder(OrderMessage message, Acknowledgment ack) {
// Extract tracing context from headers
ITransaction transaction = Sentry.startTransaction(
"kafka.consume", 
"message.consumer"
);
// Set transaction context from Kafka headers
transaction.setData("topic", "orders");
transaction.setData("partition", message.getPartition());
transaction.setData("offset", message.getOffset());
try {
processOrderMessage(message);
transaction.setStatus(SpanStatus.OK);
ack.acknowledge();
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
// Capture the error with Kafka context
SentryEvent event = new SentryEvent(e);
event.setContexts("kafka", Map.of(
"topic", "orders",
"key", message.getKey(),
"partition", message.getPartition(),
"offset", message.getOffset()
));
Sentry.captureEvent(event);
throw e;
} finally {
transaction.finish();
}
}
private void processOrderMessage(OrderMessage message) {
// Business logic for processing order message
ISpan span = Sentry.getCurrentTransaction().startChild("order.message.process");
try {
// Processing logic
span.setStatus(SpanStatus.OK);
} finally {
span.finish();
}
}
}
@Component
public class ExternalServiceMonitor {
public <T> T callExternalServiceWithMonitoring(
String serviceName, 
String operation, 
Supplier<T> operationSupplier) {
ITransaction transaction = Sentry.getCurrentTransaction();
if (transaction == null) {
return operationSupplier.get();
}
ISpan span = transaction.startChild("external.service", serviceName + "." + operation);
span.setData("service", serviceName);
span.setData("operation", operation);
long startTime = System.currentTimeMillis();
try {
T result = operationSupplier.get();
span.setStatus(SpanStatus.OK);
span.setData("duration_ms", System.currentTimeMillis() - startTime);
return result;
} catch (Exception e) {
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
span.setData("duration_ms", System.currentTimeMillis() - startTime);
throw e;
} finally {
span.finish();
}
}
}

Logback Integration

1. Logback Configuration

<!-- logback-spring.xml -->
<configuration>
<!-- Sentry Appender -->
<appender name="SENTRY" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<minimumEventLevel>WARN</minimumEventLevel>
<minimumBreadcrumbLevel>DEBUG</minimumBreadcrumbLevel>
</appender>
<!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Async Appender for Sentry -->
<appender name="ASYNC_SENTRY" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="SENTRY" />
<queueSize>1000</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
</appender>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_SENTRY" />
</root>
<!-- Specific package logging -->
<logger name="com.example.service" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_SENTRY" />
</logger>
</configuration>

2. Structured Logging with MDC

@Component
public class OrderServiceWithLogging {
private static final Logger logger = LoggerFactory.getLogger(OrderServiceWithLogging.class);
public void processOrder(Order order) {
// Set MDC context for logging
MDC.put("order_id", order.getId());
MDC.put("user_id", order.getUserId());
MDC.put("transaction_id", order.getTransactionId());
try {
logger.info("Starting order processing");
validateOrder(order);
processPayment(order);
logger.info("Order processed successfully");
} catch (PaymentException e) {
logger.error("Payment processing failed for order: {}", order.getId(), e);
throw e;
} catch (Exception e) {
logger.error("Unexpected error processing order: {}", order.getId(), e);
throw new BusinessException("ORDER_PROCESSING_FAILED", "Order processing failed", e);
} finally {
// Clear MDC
MDC.clear();
}
}
private void validateOrder(Order order) {
MDC.put("validation_step", "amount_check");
if (order.getAmount() <= 0) {
logger.warn("Invalid order amount: {}", order.getAmount());
throw new ValidationException("Invalid amount");
}
MDC.put("validation_step", "inventory_check");
if (!inventoryService.hasStock(order.getProductId(), order.getQuantity())) {
logger.warn("Insufficient stock for product: {}", order.getProductId());
throw new InventoryException("Insufficient stock");
}
logger.debug("Order validation completed successfully");
}
}

Spring Boot Integration

1. Spring Boot Configuration

@Configuration
@EnableSentry
public class SentryConfiguration {
@Bean
public ServletContextInitializer sentryServletContextInitializer() {
return new SentryServletContextInitializer();
}
@Bean
public SentryExceptionResolver sentryExceptionResolver() {
return new SentryExceptionResolver();
}
@Bean
public SentryRequestListener sentryRequestListener() {
return new SentryRequestListener();
}
}
// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
// Capture to Sentry with business context
e.captureToSentry();
ErrorResponse error = new ErrorResponse(e.getErrorCode(), e.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
// Capture unexpected errors
Sentry.captureException(e);
ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
return ResponseEntity.internalServerError().body(error);
}
}
// REST Controller with monitoring
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
ITransaction transaction = Sentry.startTransaction("http.request", "POST /api/orders");
try {
// Set transaction context
transaction.setData("user_id", request.getUserId());
transaction.setData("order_items", request.getItems().size());
Order order = orderService.processOrder(request);
transaction.setStatus(SpanStatus.OK);
return ResponseEntity.ok(new OrderResponse(order));
} catch (BusinessException e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INVALID_ARGUMENT);
throw e;
} catch (Exception e) {
transaction.setThrowable(e);
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
transaction.finish();
}
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable String orderId) {
// Auto-monitored by Sentry Spring Boot integration
Order order = orderService.findOrder(orderId);
return ResponseEntity.ok(new OrderResponse(order));
}
}

Testing and Development

1. Test Configuration

@SpringBootTest
@TestPropertySource(properties = {
"sentry.dsn=https://[email protected]/test-project",
"sentry.environment=test",
"sentry.traces-sample-rate=1.0", // Sample all traces in tests
"sentry.debug=true"
})
public class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
void shouldCapturePaymentErrors() {
// Given
Order order = createTestOrder();
when(paymentGateway.charge(any())).thenThrow(new PaymentGatewayException("Declined"));
// When & Then
BusinessException exception = assertThrows(BusinessException.class, () -> {
orderService.processOrder(order);
});
// Verify error was captured (you might check mock Sentry client in real scenario)
assertEquals("PAYMENT_FAILED", exception.getErrorCode());
}
@Test
void shouldAddBreadcrumbsDuringProcessing() {
// Given
Order order = createTestOrder();
// When
orderService.processOrder(order);
// Then - processing should complete without errors
// Breadcrumbs would be captured in real Sentry instance
}
}

2. Development/Staging Configuration

@Configuration
@Profile("dev")
public class DevSentryConfig {
@Bean
public SentryOptionsConfiguration devSentryOptions() {
return options -> {
options.setDsn("https://[email protected]/dev-project");
options.setEnvironment("development");
options.setDebug(true);
options.setTracesSampleRate(1.0); // Sample all in dev
options.setBeforeSend((event, hint) -> {
// Don't send events for expected test errors
if (isExpectedTestError(event)) {
return null;
}
return event;
});
};
}
private boolean isExpectedTestError(SentryEvent event) {
// Filter out expected test errors
return event.getThrowable() != null &&
event.getThrowable().getMessage() != null &&
event.getThrowable().getMessage().contains("TEST_ERROR");
}
}

Monitoring and Alerting

1. Custom Event Processing

@Component
public class SentryEventProcessor {
@EventListener
public void processSentryEvent(SentryEvent event) {
// Add custom logic before events are sent
enrichEventWithBusinessContext(event);
applyCustomFiltering(event);
addCustomTags(event);
}
private void enrichEventWithBusinessContext(SentryEvent event) {
// Add business-specific context
if (event.getThrowable() instanceof BusinessException) {
BusinessException be = (BusinessException) event.getThrowable();
event.setTag("business_error_code", be.getErrorCode());
event.setContexts("business_context", be.getContext());
}
// Add deployment information
event.setTag("deployment_id", System.getenv("DEPLOYMENT_ID"));
event.setTag("kubernetes_pod", System.getenv("HOSTNAME"));
}
private void applyCustomFiltering(SentryEvent event) {
// Filter out noisy errors
if (shouldFilterEvent(event)) {
event.setSkipCapture(true);
}
}
private boolean shouldFilterEvent(SentryEvent event) {
// Example filtering logic
return event.getThrowable() != null &&
(event.getThrowable() instanceof KnownNoisyException ||
event.getMessage() != null && 
event.getMessage().getMessage().contains("Ignorable error"));
}
private void addCustomTags(SentryEvent event) {
// Add custom tags for better filtering in Sentry
event.setTag("application_version", getApplicationVersion());
event.setTag("jvm_version", System.getProperty("java.version"));
event.setTag("os", System.getProperty("os.name"));
}
private String getApplicationVersion() {
return getClass().getPackage().getImplementationVersion() ?? "unknown";
}
}

2. Health Checks and Monitoring

@Component
public class SentryHealthIndicator implements HealthIndicator {
private final SentryClient sentryClient;
public SentryHealthIndicator(SentryClient sentryClient) {
this.sentryClient = sentryClient;
}
@Override
public Health health() {
try {
// Check if Sentry client is properly configured
boolean isHealthy = sentryClient.isEnabled();
if (isHealthy) {
return Health.up()
.withDetail("dsn", "configured")
.withDetail("environment", sentryClient.getOptions().getEnvironment())
.build();
} else {
return Health.down()
.withDetail("reason", "Sentry client not enabled")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
// Custom metrics for Sentry events
@Component
public class SentryMetrics {
private final MeterRegistry meterRegistry;
private final Counter capturedEvents;
private final Counter droppedEvents;
public SentryMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.capturedEvents = Counter.builder("sentry.events.captured")
.description("Number of events captured by Sentry")
.register(meterRegistry);
this.droppedEvents = Counter.builder("sentry.events.dropped")
.description("Number of events dropped by Sentry")
.register(meterRegistry);
}
public void incrementCapturedEvents() {
capturedEvents.increment();
}
public void incrementDroppedEvents() {
droppedEvents.increment();
}
}

This comprehensive Sentry integration covers everything from basic error tracking to advanced performance monitoring, logging integration, and custom instrumentation. The key is to provide rich context with each error to make debugging easier and to use performance monitoring to identify bottlenecks in your application.

Leave a Reply

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


Macro Nepal Helper