google-site-verification: google61fe8ba583a51912.html
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