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