Introduction to Sentry Performance Monitoring
Sentry Performance provides distributed tracing, performance monitoring, and transaction tracking for Java applications. It helps identify performance bottlenecks, monitor transactions, and track errors across your entire application stack.
Key Features
- Distributed Tracing: End-to-end request tracking across services
- Performance Monitoring: Latency, throughput, and Apdex scoring
- Transaction Tracking: Detailed transaction breakdown and spans
- Profiling: CPU and memory profiling for performance analysis
- Release Health: Monitor deployment impact on performance
Implementation Guide
Dependencies
Add to your pom.xml:
<properties> <sentry.version>6.28.0</sentry.version> <spring.boot.version>3.1.0</spring.boot.version> </properties> <dependencies> <!-- Sentry Core --> <dependency> <groupId>io.sentry</groupId> <artifactId>sentry</artifactId> <version>${sentry.version}</version> </dependency> <!-- Sentry Spring Boot Starter --> <dependency> <groupId>io.sentry</groupId> <artifactId>sentry-spring-boot-starter</artifactId> <version>${sentry.version}</version> </dependency> <!-- Sentry Logback Integration --> <dependency> <groupId>io.sentry</groupId> <artifactId>sentry-logback</artifactId> <version>${sentry.version}</version> </dependency> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- Spring Boot Actuator --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- Database (Example) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> Configuration
application.yml
sentry: dsn: https://[email protected]/your-project environment: ${SENTRY_ENVIRONMENT:development} release: [email protected] # Performance monitoring traces-sample-rate: 1.0 # 100% in dev, lower in prod profiles-sample-rate: 1.0 # 100% in dev, lower in prod debug: ${SENTRY_DEBUG:false} send-default-pii: true # Logging integration logging: enabled: true minimum-event-level: WARN minimum-breadcrumb-level: INFO # In-app excludes (reduce noise) in-app-includes: - com.yourcompany - com.yourapp # Server name for distributed tracing server-name: ${HOSTNAME:local} # Spring Actuator for health checks management: endpoints: web: exposure: include: health,metrics,info endpoint: health: show-details: always # Application specific app: name: sentry-performance-demo version: 1.0.0
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <!-- Sentry Appender --> <appender name="SENTRY" class="io.sentry.logback.SentryAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>WARN</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> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="SENTRY" /> </root> <!-- Custom logger for performance events --> <logger name="com.example.sentry.performance" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE" /> <appender-ref ref="SENTRY" /> </logger> </configuration> Core Performance Monitoring Setup
package com.example.sentry.config; import io.sentry.Sentry; import io.sentry.SentryOptions; import io.sentry.spring.tracing.SentryTracingConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import(SentryTracingConfiguration.class) public class SentryPerformanceConfig implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(SentryPerformanceConfig.class); @Value("${sentry.dsn:}") private String sentryDsn; @Value("${sentry.environment:development}") private String environment; @Value("${sentry.traces-sample-rate:1.0}") private Double tracesSampleRate; @Value("${sentry.profiles-sample-rate:1.0}") private Double profilesSampleRate; @Override public void afterPropertiesSet() { if (sentryDsn == null || sentryDsn.isEmpty()) { logger.warn("Sentry DSN not configured. Performance monitoring will be disabled."); return; } logger.info("Initializing Sentry Performance Monitoring for environment: {}", environment); } @Bean public SentryOptions sentryOptions() { SentryOptions options = new SentryOptions(); options.setDsn(sentryDsn); options.setEnvironment(environment); options.setTracesSampleRate(tracesSampleRate); options.setProfilesSampleRate(profilesSampleRate); options.setDebug(true); // Enable debug logs in development // Configure in-app includes for better stack traces options.addInAppInclude("com.example.sentry"); options.addInAppInclude("com.yourcompany"); // Set release information options.setRelease("[email protected]"); // Configure beforeSend to filter sensitive data options.setBeforeSend((event, hint) -> { // Filter sensitive data from events if (event.getRequest() != null) { event.getRequest().getHeaders().remove("Authorization"); event.getRequest().getHeaders().remove("Cookie"); } return event; }); return options; } } Performance Monitoring Service
package com.example.sentry.performance; import io.sentry.*; import io.sentry.protocol.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Service public class PerformanceMonitoringService { private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringService.class); private final Map<String, ITransaction> activeTransactions = new ConcurrentHashMap<>(); public ITransaction startTransaction(String name, String operation) { return startTransaction(name, operation, null); } public ITransaction startTransaction(String name, String operation, CustomSamplingContext customContext) { TransactionContext transactionContext = new TransactionContext(name, operation); // Custom sampling context if (customContext != null) { transactionContext.setCustomSamplingContext(customContext); } ITransaction transaction = Sentry.startTransaction(transactionContext); activeTransactions.put(transaction.getSpanContext().getTraceId().toString(), transaction); logger.debug("Started transaction: {} (operation: {})", name, operation); return transaction; } public ISpan startSpan(String operation, String description) { ISpan currentSpan = Sentry.getCurrentSpan(); if (currentSpan != null) { return currentSpan.startChild(operation, description); } return null; } public void setTransactionName(String transactionId, String name) { ITransaction transaction = getTransaction(transactionId); if (transaction != null) { transaction.setName(name); } } public void setTransactionTag(String transactionId, String key, String value) { ITransaction transaction = getTransaction(transactionId); if (transaction != null) { transaction.setTag(key, value); } } public void setTransactionData(String transactionId, String key, Object value) { ITransaction transaction = getTransaction(transactionId); if (transaction != null) { transaction.setData(key, value); } } public void setUserContext(String userId, String username, String email) { User user = new User(); user.setId(userId); user.setUsername(username); user.setEmail(email); Sentry.setUser(user); } public void recordBreadcrumb(String message, String category, Breadcrumb.Level level) { Breadcrumb breadcrumb = new Breadcrumb(); breadcrumb.setMessage(message); breadcrumb.setCategory(category); breadcrumb.setLevel(level); Sentry.addBreadcrumb(breadcrumb); } public void finishTransaction(String transactionId) { ITransaction transaction = activeTransactions.remove(transactionId); if (transaction != null) { transaction.finish(); logger.debug("Finished transaction: {}", transactionId); } } public void finishTransaction(ITransaction transaction) { if (transaction != null) { String traceId = transaction.getSpanContext().getTraceId().toString(); activeTransactions.remove(traceId); transaction.finish(); logger.debug("Finished transaction: {}", traceId); } } private ITransaction getTransaction(String transactionId) { return activeTransactions.get(transactionId); } public void recordMetric(String key, Number value) { Sentry.metrics().gauge(key, value); } public void recordCounter(String key, Number value) { Sentry.metrics().increment(key, value); } public void recordDistribution(String key, Number value) { Sentry.metrics().distribution(key, value); } public static class CustomSamplingContext { private final Map<String, Object> data = new HashMap<>(); public CustomSamplingContext add(String key, Object value) { data.put(key, value); return this; } public Map<String, Object> getData() { return Collections.unmodifiableMap(data); } } } Spring Boot Controllers with Performance Monitoring
package com.example.sentry.controller; import com.example.sentry.performance.PerformanceMonitoringService; import io.sentry.ITransaction; import io.sentry.SpanStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/api") public class OrderController { private static final Logger logger = LoggerFactory.getLogger(OrderController.class); @Autowired private PerformanceMonitoringService performanceService; @Autowired private OrderService orderService; @PostMapping("/orders") public ResponseEntity<Map<String, Object>> createOrder(@RequestBody CreateOrderRequest request) { // Start transaction for this request ITransaction transaction = performanceService.startTransaction( "POST /api/orders", "http.server" ); try { // Set transaction context transaction.setData("request.body", request); transaction.setTag("http.method", "POST"); transaction.setTag("http.route", "/api/orders"); // Record breadcrumb for request start performanceService.recordBreadcrumb( "Processing order creation request", "http.request", Breadcrumb.Level.INFO ); // Set user context if available if (request.getUserId() != null) { performanceService.setUserContext( request.getUserId(), request.getUsername(), request.getEmail() ); } // Business logic with spans ISpan validationSpan = performanceService.startSpan("validation", "Validate order request"); orderService.validateOrder(request); if (validationSpan != null) { validationSpan.finish(); } ISpan processingSpan = performanceService.startSpan("processing", "Process order creation"); Order order = orderService.createOrder(request); if (processingSpan != null) { processingSpan.finish(); } // Record metrics performanceService.recordMetric("orders.created", 1); performanceService.recordMetric("order.amount", request.getTotalAmount()); Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("orderId", order.getId()); response.put("message", "Order created successfully"); transaction.setTag("http.status_code", "201"); transaction.setData("response", response); performanceService.recordBreadcrumb( "Order created successfully", "order.processing", Breadcrumb.Level.INFO ); return ResponseEntity.status(201).body(response); } catch (Exception e) { // Record exception and set transaction status transaction.setThrowable(e); transaction.setStatus(SpanStatus.INTERNAL_ERROR); logger.error("Error creating order", e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("status", "error"); errorResponse.put("message", e.getMessage()); return ResponseEntity.status(500).body(errorResponse); } finally { performanceService.finishTransaction(transaction); } } @GetMapping("/orders/{orderId}") public ResponseEntity<Map<String, Object>> getOrder(@PathVariable String orderId) { ITransaction transaction = performanceService.startTransaction( "GET /api/orders/{orderId}", "http.server" ); try { transaction.setTag("order.id", orderId); transaction.setTag("http.method", "GET"); transaction.setTag("http.route", "/api/orders/{orderId}"); performanceService.recordBreadcrumb( "Fetching order details for: " + orderId, "order.query", Breadcrumb.Level.INFO ); ISpan dbSpan = performanceService.startSpan("database", "Query order from database"); Order order = orderService.findOrderById(orderId); if (dbSpan != null) { dbSpan.finish(); } if (order == null) { transaction.setTag("http.status_code", "404"); transaction.setStatus(SpanStatus.NOT_FOUND); Map<String, Object> response = new HashMap<>(); response.put("status", "error"); response.put("message", "Order not found"); return ResponseEntity.status(404).body(response); } Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("order", order); transaction.setTag("http.status_code", "200"); transaction.setStatus(SpanStatus.OK); return ResponseEntity.ok(response); } catch (Exception e) { transaction.setThrowable(e); transaction.setStatus(SpanStatus.INTERNAL_ERROR); logger.error("Error fetching order: {}", orderId, e); throw e; } finally { performanceService.finishTransaction(transaction); } } @GetMapping("/orders") public ResponseEntity<Map<String, Object>> listOrders( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { ITransaction transaction = performanceService.startTransaction( "GET /api/orders", "http.server" ); try { transaction.setData("page", page); transaction.setData("size", size); transaction.setTag("http.method", "GET"); performanceService.recordBreadcrumb( String.format("Listing orders - page: %d, size: %d", page, size), "order.list", Breadcrumb.Level.INFO ); ISpan dbSpan = performanceService.startSpan("database", "Query orders with pagination"); List<Order> orders = orderService.findOrders(page, size); if (dbSpan != null) { dbSpan.finish(); } Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("orders", orders); response.put("page", page); response.put("size", size); transaction.setTag("http.status_code", "200"); transaction.setData("orders.count", orders.size()); // Record performance metric performanceService.recordMetric("orders.listed", orders.size()); return ResponseEntity.ok(response); } catch (Exception e) { transaction.setThrowable(e); transaction.setStatus(SpanStatus.INTERNAL_ERROR); logger.error("Error listing orders", e); throw e; } finally { performanceService.finishTransaction(transaction); } } } Service Layer with Performance Instrumentation
package com.example.sentry.service; import com.example.sentry.performance.PerformanceMonitoringService; import io.sentry.ISpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); @Autowired private PerformanceMonitoringService performanceService; @Autowired private OrderRepository orderRepository; @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService; @Transactional public Order createOrder(CreateOrderRequest request) { ISpan orderSpan = performanceService.startSpan("order.creation", "Create new order"); try { performanceService.recordBreadcrumb( "Starting order creation process", "order.creation", Breadcrumb.Level.INFO ); // Validate inventory ISpan inventorySpan = performanceService.startSpan("inventory.check", "Check product availability"); try { inventoryService.checkAvailability(request.getItems()); } finally { if (inventorySpan != null) { inventorySpan.finish(); } } // Process payment ISpan paymentSpan = performanceService.startSpan("payment.processing", "Process payment"); boolean paymentSuccess; try { paymentSuccess = paymentService.processPayment( request.getPaymentMethod(), request.getTotalAmount() ); } finally { if (paymentSpan != null) { paymentSpan.finish(); } } if (!paymentSuccess) { throw new RuntimeException("Payment processing failed"); } // Create order entity ISpan dbSpan = performanceService.startSpan("database.save", "Save order to database"); Order order; try { order = new Order(); order.setCustomerId(request.getUserId()); order.setItems(request.getItems()); order.setTotalAmount(request.getTotalAmount()); order.setStatus("CREATED"); order = orderRepository.save(order); performanceService.recordBreadcrumb( "Order saved to database with ID: " + order.getId(), "order.persistence", Breadcrumb.Level.INFO ); } finally { if (dbSpan != null) { dbSpan.finish(); } } // Update inventory ISpan updateSpan = performanceService.startSpan("inventory.update", "Update inventory levels"); try { inventoryService.updateInventory(request.getItems()); } finally { if (updateSpan != null) { updateSpan.finish(); } } performanceService.recordBreadcrumb( "Order creation completed successfully", "order.creation", Breadcrumb.Level.INFO ); return order; } catch (Exception e) { performanceService.recordBreadcrumb( "Order creation failed: " + e.getMessage(), "order.creation", Breadcrumb.Level.ERROR ); throw e; } finally { if (orderSpan != null) { orderSpan.finish(); } } } public Order findOrderById(String orderId) { ISpan span = performanceService.startSpan("database.query", "Find order by ID"); try { performanceService.recordBreadcrumb( "Querying order by ID: " + orderId, "order.query", Breadcrumb.Level.INFO ); Optional<Order> order = orderRepository.findById(orderId); if (order.isPresent()) { performanceService.recordBreadcrumb( "Order found: " + orderId, "order.query", Breadcrumb.Level.INFO ); } else { performanceService.recordBreadcrumb( "Order not found: " + orderId, "order.query", Breadcrumb.Level.WARNING ); } return order.orElse(null); } finally { if (span != null) { span.finish(); } } } public List<Order> findOrders(int page, int size) { ISpan span = performanceService.startSpan("database.query", "Find orders with pagination"); try { performanceService.recordBreadcrumb( String.format("Querying orders - page: %d, size: %d", page, size), "order.list", Breadcrumb.Level.INFO ); // Simulate database query with pagination List<Order> orders = orderRepository.findWithPagination(page, size); performanceService.recordBreadcrumb( String.format("Found %d orders", orders.size()), "order.list", Breadcrumb.Level.INFO ); return orders; } finally { if (span != null) { span.finish(); } } } public void validateOrder(CreateOrderRequest request) { ISpan span = performanceService.startSpan("validation", "Validate order request"); try { if (request.getUserId() == null || request.getUserId().trim().isEmpty()) { throw new IllegalArgumentException("User ID is required"); } if (request.getItems() == null || request.getItems().isEmpty()) { throw new IllegalArgumentException("Order items are required"); } if (request.getTotalAmount() == null || request.getTotalAmount().doubleValue() <= 0) { throw new IllegalArgumentException("Valid total amount is required"); } performanceService.recordBreadcrumb( "Order validation passed", "order.validation", Breadcrumb.Level.INFO ); } finally { if (span != null) { span.finish(); } } } } Database Repository with Performance Monitoring
package com.example.sentry.repository; import com.example.sentry.performance.PerformanceMonitoringService; import io.sentry.ISpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Repository public class OrderRepository { private static final Logger logger = LoggerFactory.getLogger(OrderRepository.class); // In-memory storage for demo purposes private final Map<String, Order> orderStorage = new ConcurrentHashMap<>(); @Autowired private PerformanceMonitoringService performanceService; public Order save(Order order) { ISpan span = performanceService.startSpan("database.save", "Save order to storage"); try { if (order.getId() == null) { order.setId(UUID.randomUUID().toString()); } // Simulate database latency simulateDatabaseLatency(); orderStorage.put(order.getId(), order); performanceService.recordBreadcrumb( "Order saved with ID: " + order.getId(), "database.operation", Breadcrumb.Level.INFO ); return order; } finally { if (span != null) { span.finish(); } } } public Optional<Order> findById(String id) { ISpan span = performanceService.startSpan("database.query", "Find order by ID"); try { // Simulate database latency simulateDatabaseLatency(); Order order = orderStorage.get(id); if (order != null) { performanceService.recordBreadcrumb( "Order found: " + id, "database.operation", Breadcrumb.Level.INFO ); } else { performanceService.recordBreadcrumb( "Order not found: " + id, "database.operation", Breadcrumb.Level.INFO ); } return Optional.ofNullable(order); } finally { if (span != null) { span.finish(); } } } public List<Order> findWithPagination(int page, int size) { ISpan span = performanceService.startSpan("database.query", "Find orders with pagination"); try { // Simulate database latency simulateDatabaseLatency(); List<Order> allOrders = new ArrayList<>(orderStorage.values()); // Simple pagination int start = page * size; int end = Math.min(start + size, allOrders.size()); if (start >= allOrders.size()) { return Collections.emptyList(); } List<Order> paginatedOrders = allOrders.subList(start, end); performanceService.recordBreadcrumb( String.format("Paginated query: %d orders from position %d", paginatedOrders.size(), start), "database.operation", Breadcrumb.Level.INFO ); return paginatedOrders; } finally { if (span != null) { span.finish(); } } } private void simulateDatabaseLatency() { try { // Simulate database operation latency (50-200ms) Thread.sleep(50 + (long)(Math.random() * 150)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.warn("Database latency simulation interrupted"); } } } Async Service with Performance Context Propagation
package com.example.sentry.service; import com.example.sentry.performance.PerformanceMonitoringService; import io.sentry.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; @Service public class AsyncOrderProcessor { private static final Logger logger = LoggerFactory.getLogger(AsyncOrderProcessor.class); @Autowired private PerformanceMonitoringService performanceService; @Async public CompletableFuture<String> processOrderAsync(String orderId) { // Capture current Sentry context for async propagation ITransaction currentTransaction = Sentry.getCurrentTransaction(); SpanContext spanContext = currentTransaction != null ? currentTransaction.getSpanContext() : null; return CompletableFuture.supplyAsync(() -> { // Create new transaction for async work TransactionContext transactionContext = new TransactionContext( "async.order.processing", "background.task" ); if (spanContext != null) { transactionContext.setParentSpanId(spanContext.getSpanId()); transactionContext.setTraceId(spanContext.getTraceId()); } ITransaction asyncTransaction = Sentry.startTransaction(transactionContext); try { asyncTransaction.setTag("order.id", orderId); asyncTransaction.setTag("processing.type", "async"); performanceService.recordBreadcrumb( "Starting async order processing for: " + orderId, "async.processing", Breadcrumb.Level.INFO ); // Simulate async processing work ISpan processingSpan = performanceService.startSpan("async.processing", "Process order asynchronously"); try { Thread.sleep(1000); // Simulate work logger.info("Async processing completed for order: {}", orderId); performanceService.recordBreadcrumb( "Async processing completed for: " + orderId, "async.processing", Breadcrumb.Level.INFO ); return "Order " + orderId + " processed asynchronously"; } finally { if (processingSpan != null) { processingSpan.finish(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); asyncTransaction.setThrowable(e); asyncTransaction.setStatus(SpanStatus.INTERNAL_ERROR); throw new RuntimeException("Async processing interrupted", e); } catch (Exception e) { asyncTransaction.setThrowable(e); asyncTransaction.setStatus(SpanStatus.INTERNAL_ERROR); throw e; } finally { performanceService.finishTransaction(asyncTransaction); } }); } } Custom Performance Filters
package com.example.sentry.filter; import com.example.sentry.performance.PerformanceMonitoringService; import io.sentry.Sentry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @Component public class PerformanceMonitoringFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringFilter.class); @Autowired private PerformanceMonitoringService performanceService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Skip for health checks and static resources if (shouldSkipMonitoring(request)) { filterChain.doFilter(request, response); return; } String transactionName = request.getMethod() + " " + request.getRequestURI(); // Start transaction PerformanceMonitoringService.CustomSamplingContext samplingContext = new PerformanceMonitoringService.CustomSamplingContext() .add("http.method", request.getMethod()) .add("http.url", request.getRequestURL().toString()) .add("user.agent", request.getHeader("User-Agent")); ITransaction transaction = performanceService.startTransaction( transactionName, "http.server", samplingContext ); try { // Add request details to transaction transaction.setTag("http.method", request.getMethod()); transaction.setTag("http.route", request.getRequestURI()); transaction.setTag("http.user_agent", request.getHeader("User-Agent")); Map<String, String> headers = new HashMap<>(); Collections.list(request.getHeaderNames()) .forEach(headerName -> headers.put(headerName, request.getHeader(headerName))); transaction.setData("request.headers", headers); // Record breadcrumb for request start performanceService.recordBreadcrumb( String.format("%s %s", request.getMethod(), request.getRequestURI()), "http.request", Breadcrumb.Level.INFO ); // Continue with the filter chain filterChain.doFilter(request, response); // Set response status transaction.setTag("http.status_code", String.valueOf(response.getStatus())); if (response.getStatus() >= 400) { transaction.setStatus(SpanStatus.fromHttpStatusCode(response.getStatus())); } else { transaction.setStatus(SpanStatus.OK); } } catch (Exception e) { // Record exception transaction.setThrowable(e); transaction.setStatus(SpanStatus.INTERNAL_ERROR); throw e; } finally { performanceService.finishTransaction(transaction); } } private boolean shouldSkipMonitoring(HttpServletRequest request) { String path = request.getRequestURI(); return path.startsWith("/actuator") || path.startsWith("/health") || path.contains("."); // static resources } } Performance Metrics Collector
package com.example.sentry.metrics; import io.sentry.Sentry; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.lang.management.*; import java.util.concurrent.atomic.AtomicLong; @Component public class PerformanceMetricsCollector { private final AtomicLong totalOrders = new AtomicLong(0); private final AtomicLong failedOrders = new AtomicLong(0); @Scheduled(fixedRate = 60000) // Every minute public void collectSystemMetrics() { collectMemoryMetrics(); collectGCMetrics(); collectThreadMetrics(); collectCustomMetrics(); } private void collectMemoryMetrics() { MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean(); // Heap memory usage MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage(); Sentry.metrics().gauge("jvm.memory.heap.used", heapUsage.getUsed()); Sentry.metrics().gauge("jvm.memory.heap.committed", heapUsage.getCommitted()); Sentry.metrics().gauge("jvm.memory.heap.max", heapUsage.getMax()); // Non-heap memory usage MemoryUsage nonHeapUsage = memoryMxBean.getNonHeapMemoryUsage(); Sentry.metrics().gauge("jvm.memory.nonheap.used", nonHeapUsage.getUsed()); Sentry.metrics().gauge("jvm.memory.nonheap.committed", nonHeapUsage.getCommitted()); } private void collectGCMetrics() { for (GarbageCollectorMXBean gcMxBean : ManagementFactory.getGarbageCollectorMXBeans()) { String gcName = gcMxBean.getName().replace(" ", "_").toLowerCase(); Sentry.metrics().gauge("jvm.gc." + gcName + ".count", gcMxBean.getCollectionCount()); Sentry.metrics().gauge("jvm.gc." + gcName + ".time", gcMxBean.getCollectionTime()); } } private void collectThreadMetrics() { ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); Sentry.metrics().gauge("jvm.threads.count", threadMxBean.getThreadCount()); Sentry.metrics().gauge("jvm.threads.daemon", threadMxBean.getDaemonThreadCount()); Sentry.metrics().gauge("jvm.threads.peak", threadMxBean.getPeakThreadCount()); } private void collectCustomMetrics() { // Custom business metrics double successRate = totalOrders.get() > 0 ? (double) (totalOrders.get() - failedOrders.get()) / totalOrders.get() : 1.0; Sentry.metrics().gauge("orders.total", totalOrders.get()); Sentry.metrics().gauge("orders.failed", failedOrders.get()); Sentry.metrics().gauge("orders.success_rate", successRate); } public void recordOrder(boolean success) { totalOrders.incrementAndGet(); if (!success) { failedOrders.incrementAndGet(); } } } Testing Configuration
package com.example.sentry.config; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; @TestConfiguration public class TestSentryConfig { @Bean @Primary public PerformanceMonitoringService testPerformanceService() { // Mock or test implementation return new PerformanceMonitoringService(); } } Main Application Class
package com.example.sentry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableAsync @EnableScheduling public class SentryPerformanceApplication { public static void main(String[] args) { SpringApplication.run(SentryPerformanceApplication.class, args); } } Testing
package com.example.sentry.controller; import com.example.sentry.SentryPerformanceApplication; import com.example.sentry.config.TestSentryConfig; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ContextConfiguration(classes = {SentryPerformanceApplication.class, TestSentryConfig.class}) class OrderControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Test void testCreateOrder() throws Exception { String orderJson = """ { "userId": "user-123", "username": "testuser", "email": "[email protected]", "items": [ { "productId": "prod-1", "quantity": 2, "price": 25.50 } ], "totalAmount": 51.00, "paymentMethod": "credit_card" } """; mockMvc.perform(post("/api/orders") .contentType(MediaType.APPLICATION_JSON) .content(orderJson)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.status").value("success")) .andExpect(jsonPath("$.orderId").exists()); } @Test void testGetOrderNotFound() throws Exception { mockMvc.perform(get("/api/orders/nonexistent")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("error")); } } Best Practices
1. Transaction Sampling
@Component public class SmartSampling { public boolean shouldSampleTransaction(HttpServletRequest request) { String path = request.getRequestURI(); // Sample all critical paths if (path.startsWith("/api/orders") || path.startsWith("/api/payments")) { return true; } // Sample only 10% of health checks if (path.startsWith("/health")) { return Math.random() < 0.1; } // Default sampling rate return Math.random() < 0.5; } } 2. Sensitive Data Filtering
@Configuration public class SentrySecurityConfig { @Bean public SentryOptions.BeforeSendCallback beforeSendCallback() { return (event, hint) -> { // Remove sensitive headers if (event.getRequest() != null) { event.getRequest().getHeaders().remove("Authorization"); event.getRequest().getHeaders().remove("Cookie"); event.getRequest().getHeaders().remove("X-API-Key"); } // Scrub sensitive data from breadcrumbs if (event.getBreadcrumbs() != null) { event.getBreadcrumbs().forEach(breadcrumb -> { if (breadcrumb.getMessage() != null) { breadcrumb.setMessage( breadcrumb.getMessage() .replaceAll("(?i)password=[^&]+", "password=***") .replaceAll("(?i)token=[^&]+", "token=***") ); } }); } return event; }; } } Conclusion
This comprehensive Sentry Performance implementation for Java provides:
- Automatic instrumentation for Spring Boot applications
- Distributed tracing across service boundaries
- Custom performance monitoring for business logic
- Async context propagation for background tasks
- Performance metrics collection and monitoring
- Security-sensitive data filtering
- Testing support for development
Key benefits include reduced mean time to detection (MTTD) for performance issues, detailed transaction breakdowns, and comprehensive monitoring across your entire application stack. The implementation follows Sentry best practices while providing flexibility for custom performance monitoring needs.
Remember to:
- Configure appropriate sampling rates for production
- Filter sensitive data before sending to Sentry
- Use meaningful transaction and span names
- Monitor key business metrics alongside technical metrics
- Test performance monitoring in development environments