Java Flight Recorder (JFR) is a powerful profiling and diagnostics tool built into the JVM. While it captures extensive system and JVM metrics out-of-the-box, its true power emerges when you extend it with custom events to monitor your application-specific logic. This guide covers how to define, instrument, and analyze custom JFR events.
Understanding Custom JFR Events
Custom JFR events allow you to record application-specific metrics, business transactions, and domain-specific operations alongside JVM telemetry. This provides a complete picture of your application's behavior in production.
When to Use Custom Events:
- Track business transaction latency
- Monitor cache hit/miss ratios
- Record domain-specific counters
- Trace custom workflow execution
- Log important business milestones
Defining Custom Events
1. Basic Event Definition
The simplest way to create a custom event is by extending the jdk.jfr.Event class.
import jdk.jfr.*; @Name("com.example.PaymentProcessingEvent") @Label("Payment Processing") @Category("Business") @Description("Tracks payment processing execution") public class PaymentProcessingEvent extends Event { @Label("Payment ID") @Description("Unique payment identifier") public String paymentId; @Label("Amount") @Description("Payment amount in cents") public long amount; @Label("Currency") public String currency; @Label("Processing Time") @Description("Time taken to process payment in milliseconds") public long processingTime; @Label("Success") public boolean success; @Label("Error Message") public String errorMessage; } 2. Using Annotations for Metadata
JFR provides annotations to control how events are recorded and displayed:
import jdk.jfr.*; import java.lang.annotation.*; @Name("com.example.OrderEvent") @Label("Order Processing") @Category({"E-Commerce", "Business"}) @Description("Captures order processing lifecycle events") @StackTrace(true) // Include stack trace @Enabled(true) // Enabled by default @Registered(true) // Automatically registered public class OrderEvent extends Event { @Label("Order ID") @Description("Unique order identifier") public String orderId; @Label("Customer ID") public String customerId; @Label("Total Amount") @Description("Order total in smallest currency unit") public long totalAmount; @Label("Item Count") public int itemCount; @Label("Event Type") @Description("Type of order event: CREATED, PROCESSED, COMPLETED, FAILED") public String eventType; @Label("Payment Method") public String paymentMethod; // Relational field to correlate events @Label("Correlation ID") @Relational public String correlationId; } 3. Advanced Event Configuration
Control event timing, thresholds, and periodicity:
@Name("com.example.CacheEvent") @Label("Cache Operation") @Category("Performance") @StackTrace(false) @Enabled(true) @Threshold("10 ms") // Only record if duration > 10ms @Period("everyChunk") // Controls when event is written public class CacheEvent extends Event { @Label("Cache Name") public String cacheName; @Label("Operation") @Description("Cache operation: GET, PUT, REMOVE, EVICT") public String operation; @Label("Key") public String key; @Label("Hit") public boolean hit; @Label("Value Size") @Description("Size of cached value in bytes") public long valueSize; @Label("Duration") @Description("Operation duration in nanoseconds") public long duration; // Use unsigned for values that shouldn't be negative @Label("Cache Size") @Unsigned public long cacheSize; } Instrumenting Code with Custom Events
1. Simple Event Emission
public class PaymentService { public PaymentResult processPayment(PaymentRequest request) { // Create event instance PaymentProcessingEvent event = new PaymentProcessingEvent(); event.paymentId = request.getId(); event.amount = request.getAmount(); event.currency = request.getCurrency(); // Start timing long startTime = System.nanoTime(); event.begin(); // Mark event start try { // Business logic PaymentResult result = paymentGateway.charge(request); event.success = true; return result; } catch (PaymentException e) { event.success = false; event.errorMessage = e.getMessage(); throw e; } finally { // End timing and commit event event.end(); // Mark event end event.processingTime = (System.nanoTime() - startTime) / 1_000_000; // Event is automatically committed when it goes out of scope // or you can explicitly call event.commit(); } } } 2. Using Try-With-Resources Pattern
For simpler timing of code blocks:
public class OrderService { public void createOrder(Order order) { OrderEvent event = new OrderEvent(); event.orderId = order.getId(); event.customerId = order.getCustomerId(); event.totalAmount = order.getTotalAmount(); event.itemCount = order.getItemCount(); event.eventType = "CREATED"; event.paymentMethod = order.getPaymentMethod(); // Automatically times the block and commits try (EventScope es = EventScope.open(event)) { inventoryService.reserveItems(order.getItems()); paymentService.authorize(order); orderRepository.save(order); event.eventType = "PROCESSED"; } catch (Exception e) { event.eventType = "FAILED"; event.errorMessage = e.getMessage(); throw e; } } } // Helper class for try-with-resources pattern class EventScope implements AutoCloseable { private final Event event; private EventScope(Event event) { this.event = event; this.event.begin(); } public static EventScope open(Event event) { return new EventScope(event); } @Override public void close() { event.end(); event.commit(); } } 3. Instant Events (Without Duration)
For events that represent moments in time:
@Name("com.example.UserLoginEvent") @Label("User Login") @Category("Security") public class UserLoginEvent extends Event { @Label("User ID") public String userId; @Label("IP Address") public String ipAddress; @Label("User Agent") public String userAgent; @Label("Success") public boolean success; } // Usage public class AuthService { public boolean login(String username, String password, String ip) { UserLoginEvent event = new UserLoginEvent(); event.userId = username; event.ipAddress = ip; event.userAgent = getCurrentUserAgent(); boolean success = authenticate(username, password); event.success = success; event.commit(); // Instant event - no begin/end needed return success; } } Advanced Event Patterns
1. Request Scoped Events with Correlation
@Name("com.example.RequestEvent") @Label("HTTP Request Processing") @Category("Web") public class RequestEvent extends Event { @Label("Request ID") @Relational public String requestId; @Label("HTTP Method") public String method; @Label("URI") public String uri; @Label("Status Code") public int statusCode; @Label("Client IP") public String clientIp; @Label("Duration") public long duration; @Label("Response Size") @Unsigned public long responseSize; } // Using in a web filter public class JfrRequestFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; RequestEvent event = new RequestEvent(); event.requestId = generateRequestId(); event.method = httpRequest.getMethod(); event.uri = httpRequest.getRequestURI(); event.clientIp = httpRequest.getRemoteAddr(); long startTime = System.nanoTime(); event.begin(); try { chain.doFilter(request, response); event.statusCode = httpResponse.getStatus(); } finally { event.end(); event.duration = System.nanoTime() - startTime; // Capture response size if possible if (response instanceof ContentCachingResponseWrapper) { event.responseSize = ((ContentCachingResponseWrapper) response).getContentSize(); } event.commit(); } } } 2. Custom Periodic Events
For events that should be recorded at regular intervals:
@Name("com.example.ApplicationMetricsEvent") @Label("Application Metrics") @Category("Metrics") @Period("1 s") // Emit every second public class ApplicationMetricsEvent extends Event { @Label("Active Users") @Description("Number of currently active users") public int activeUsers; @Label("Pending Orders") public int pendingOrders; @Label("Cache Hit Rate") @Description("Cache hit rate as percentage") public double cacheHitRate; @Label("Database Connections") public int databaseConnections; @Label("Heap Usage") @Description("Application heap usage in bytes") public long heapUsage; } // Scheduled metrics collection @Component public class MetricsCollector { private final ApplicationMetricsEvent event = new ApplicationMetricsEvent(); @Scheduled(fixedRate = 1000) public void collectMetrics() { event.activeUsers = userSessionService.getActiveUserCount(); event.pendingOrders = orderService.getPendingOrderCount(); event.cacheHitRate = cacheService.getHitRate() * 100; event.databaseConnections = dataSource.getActiveConnections(); event.heapUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); event.commit(); } } Configuration and Control
1. Programmatic Event Control
public class JfrEventManager { public static void enableEvents() { // Enable specific event types try { FlightRecorder.getFlightRecorder() .getEventTypes() .stream() .filter(et -> et.getName().startsWith("com.example.")) .forEach(et -> { et.setEnabled(true); }); } catch (Exception e) { logger.warn("Failed to enable JFR events", e); } } public static void configureRecording() { // Create custom recording configuration Configuration config = Configuration.create( Map.of( "com.example.PaymentProcessingEvent", "enabled=true,threshold=0ms", "com.example.OrderEvent", "enabled=true,threshold=0ms", "com.example.CacheEvent", "enabled=true,threshold=1ms" ) ); Recording recording = new Recording(config); recording.start(); } } 2. Dynamic Event Enablement
@Name("com.example.DynamicEvent") @Label("Dynamic Event") @Enabled(false) // Disabled by default public class DynamicEvent extends Event { @Label("Operation") public String operation; @Label("Value") public String value; private static volatile boolean ENABLED = false; public static void setEnabled(boolean enabled) { ENABLED = enabled; // Also enable the event type at JFR level try { FlightRecorder.getFlightRecorder() .getEventTypes() .stream() .filter(et -> et.getName().equals("com.example.DynamicEvent")) .findFirst() .ifPresent(et -> et.setEnabled(enabled)); } catch (Exception e) { // Handle exception } } public static void record(String operation, String value) { if (ENABLED) { DynamicEvent event = new DynamicEvent(); event.operation = operation; event.value = value; event.commit(); } } } Best Practices
1. Performance Considerations
public class OptimizedEventUsage { // Reuse event instances when possible (thread-local) private static final ThreadLocal<CacheEvent> CACHE_EVENT = ThreadLocal.withInitial(CacheEvent::new); public Object getFromCache(String key) { // Skip event creation if event type is disabled if (!CacheEvent.class.isEnabled()) { return cache.get(key); } CacheEvent event = CACHE_EVENT.get(); event.reset(); // Clear previous values event.cacheName = "main-cache"; event.operation = "GET"; event.key = key; long start = System.nanoTime(); event.begin(); try { Object value = cache.get(key); event.hit = (value != null); event.valueSize = estimateSize(value); return value; } finally { event.end(); event.duration = System.nanoTime() - start; event.commit(); } } } 2. Naming and Organization
- Use reverse domain name notation for event names
- Group related events with categories
- Provide meaningful labels and descriptions
- Use consistent field naming conventions
3. Error Handling
public class SafeEventRecording { public static void recordSafe(Runnable eventRecording) { try { eventRecording.run(); } catch (Throwable t) { // Log but don't disrupt application flow logger.debug("Failed to record JFR event", t); } } // Usage public void businessOperation() { // ... business logic SafeEventRecording.recordSafe(() -> { BusinessEvent event = new BusinessEvent(); event.operation = "important"; event.value = "data"; event.commit(); }); } } Analysis and Visualization
1. Querying Custom Events with JMC
Java Mission Control provides powerful visualization for custom events:
-- In JMC JFR Analysis view: SELECT * FROM com.example.PaymentProcessingEvent WHERE success = false AND processingTime > 1000
2. Programmatic Analysis
public class JfrAnalysis { public void analyzeRecording(Path recordingFile) throws Exception { try (RecordingFile file = new RecordingFile(recordingFile)) { while (file.hasMoreEvents()) { RecordedEvent event = file.readEvent(); if ("com.example.PaymentProcessingEvent".equals(event.getEventType().getName())) { String paymentId = event.getString("paymentId"); long processingTime = event.getLong("processingTime"); boolean success = event.getBoolean("success"); System.out.printf("Payment %s took %d ms, success: %s%n", paymentId, processingTime, success); } } } } } Conclusion
Custom JFR events transform Java Flight Recorder from a JVM monitoring tool into a comprehensive application observability platform. By instrumenting your business logic with meaningful events, you gain:
- Deep visibility into application behavior
- Correlation between business and technical metrics
- Production debugging capabilities without log noise
- Performance baselining for business transactions
The key to successful custom event implementation is starting small—identify a few critical business workflows, instrument them with well-designed events, and gradually expand as you become comfortable with the analysis patterns and performance characteristics.