Java Flight Recorder (JFR) is a powerful profiling and diagnostics tool in the JVM. Custom JFR events allow you to instrument your application with domain-specific events that appear alongside JVM events in JFR recordings.
Table of Contents
- Basic Custom Events
- Event Configuration and Annotations
- Advanced Event Types
- Programmatic Event Control
- Real-World Use Cases
- Event Processing and Analysis
1. Basic Custom Events
Example 1: Simple Custom Event
import jdk.jfr.*; import java.time.Duration; import java.util.concurrent.TimeUnit; // Define a custom JFR event @Name("com.company.OrderProcessingEvent") @Label("Order Processing") @Category({"Business", "Order Management"}) @Description("Tracks order processing lifecycle") public class OrderProcessingEvent extends Event { @Label("Order ID") @Description("Unique order identifier") private String orderId; @Label("Customer ID") private String customerId; @Label("Order Amount") @Description("Total order amount in USD") private double amount; @Label("Processing Time") @Description("Time taken to process the order") private long processingTimeMs; @Label("Success") @Description("Whether order processing succeeded") private boolean success; @Label("Failure Reason") @Description("Reason for failure if processing failed") private String failureReason; // Getters and setters public String getOrderId() { return orderId; } public void setOrderId(String orderId) { this.orderId = orderId; } public String getCustomerId() { return customerId; } public void setCustomerId(String customerId) { this.customerId = customerId; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } public long getProcessingTimeMs() { return processingTimeMs; } public void setProcessingTimeMs(long processingTimeMs) { this.processingTimeMs = processingTimeMs; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getFailureReason() { return failureReason; } public void setFailureReason(String failureReason) { this.failureReason = failureReason; } // Utility method to easily record events public static void recordOrderProcessing( String orderId, String customerId, double amount, long processingTimeMs, boolean success, String failureReason) { OrderProcessingEvent event = new OrderProcessingEvent(); event.begin(); // Start timing // Set event fields event.orderId = orderId; event.customerId = customerId; event.amount = amount; event.processingTimeMs = processingTimeMs; event.success = success; event.failureReason = failureReason; event.commit(); // Record the event } } // Usage example public class OrderService { public void processOrder(Order order) { long startTime = System.nanoTime(); boolean success = false; String failureReason = null; try { // Simulate order processing validateOrder(order); processPayment(order); updateInventory(order); sendConfirmation(order); success = true; } catch (Exception e) { failureReason = e.getMessage(); throw e; } finally { long processingTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); // Record custom JFR event OrderProcessingEvent.recordOrderProcessing( order.getId(), order.getCustomerId(), order.getAmount(), processingTimeMs, success, failureReason ); } } private void validateOrder(Order order) { // Validation logic if (order.getAmount() <= 0) { throw new IllegalArgumentException("Invalid order amount"); } } private void processPayment(Order order) { // Payment processing logic if (order.getAmount() > 1000) { throw new RuntimeException("Payment declined"); } } private void updateInventory(Order order) { // Inventory update logic } private void sendConfirmation(Order order) { // Confirmation logic } // Simple Order class for demonstration public static class Order { private String id; private String customerId; private double amount; public Order(String id, String customerId, double amount) { this.id = id; this.customerId = customerId; this.amount = amount; } // Getters public String getId() { return id; } public String getCustomerId() { return customerId; } public double getAmount() { return amount; } } } Example 2: Timed Event with Duration
import jdk.jfr.*; import java.util.concurrent.*; @Name("com.company.DatabaseQueryEvent") @Label("Database Query Execution") @Category({"Database", "Performance"}) @Description("Tracks database query execution metrics") public class DatabaseQueryEvent extends Event { @Label("Query ID") private String queryId; @Label("SQL Query") @Description("The SQL query that was executed") private String sql; @Label("Table Name") private String tableName; @Label("Rows Affected") private int rowsAffected; @Label("Execution Time") @Description("Total query execution time in milliseconds") private long executionTimeMs; @Label("Success") private boolean success; @Label("Error Message") private String errorMessage; // Getters and setters public String getQueryId() { return queryId; } public void setQueryId(String queryId) { this.queryId = queryId; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public String getTableName() { return tableName; } public void setTableName(String tableName) { this.tableName = tableName; } public int getRowsAffected() { return rowsAffected; } public void setRowsAffected(int rowsAffected) { this.rowsAffected = rowsAffected; } public long getExecutionTimeMs() { return executionTimeMs; } public void setExecutionTimeMs(long executionTimeMs) { this.executionTimeMs = executionTimeMs; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } // Auto-timing utility method public static <T> T trackQuery(String queryId, String sql, String tableName, Callable<T> queryExecutor) throws Exception { DatabaseQueryEvent event = new DatabaseQueryEvent(); event.begin(); event.queryId = queryId; event.sql = sql; event.tableName = tableName; long startTime = System.nanoTime(); try { T result = queryExecutor.call(); event.success = true; event.rowsAffected = 1; // In real scenario, get from result return result; } catch (Exception e) { event.success = false; event.errorMessage = e.getMessage(); throw e; } finally { event.executionTimeMs = TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - startTime); event.commit(); } } } // Usage in database service public class DatabaseService { public User findUserById(String userId) throws Exception { String sql = "SELECT * FROM users WHERE id = ?"; return DatabaseQueryEvent.trackQuery( "FIND_USER_BY_ID", sql, "users", () -> { // Simulate database query Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100)); if ("error".equals(userId)) { throw new SQLException("User not found"); } return new User(userId, "John Doe"); } ); } public static class User { private String id; private String name; public User(String id, String name) { this.id = id; this.name = name; } } } 2. Event Configuration and Annotations
Example 3: Comprehensive Event Annotations
import jdk.jfr.*; import java.lang.annotation.*; // Custom annotation for method tracing @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Name("com.company.MethodTrace") @Label("Method Execution Trace") @Category("Performance") @StackTrace(true) @Enabled(true) @Registered(true) public @interface TraceMethod { String value() default ""; boolean trackArguments() default false; Threshold("10 ms") // Only record if execution takes more than 10ms } // Method tracing event @Name("com.company.MethodExecutionEvent") @Label("Method Execution") @Category({"Performance", "Method Tracing"}) @Description("Tracks method execution performance") @StackTrace(true) // Include stack trace @Enabled(true) // Enabled by default @Registered(true) // Registered with JFR @Threshold("1 ms") // Only record if duration >= 1ms public class MethodExecutionEvent extends Event { @Label("Class Name") @Description("Fully qualified class name") private String className; @Label("Method Name") private String methodName; @Label("Arguments") @Description("Method arguments as string") private String arguments; @Label("Return Value") private String returnValue; @Label("Execution Time") @Timespan(Timespan.MILLISECONDS) private long executionTimeMs; @Label("Exception") private String exception; // Getters and setters public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } public String getArguments() { return arguments; } public void setArguments(String arguments) { this.arguments = arguments; } public String getReturnValue() { return returnValue; } public void setReturnValue(String returnValue) { this.returnValue = returnValue; } public long getExecutionTimeMs() { return executionTimeMs; } public void setExecutionTimeMs(long executionTimeMs) { this.executionTimeMs = executionTimeMs; } public String getException() { return exception; } public void setException(String exception) { this.exception = exception; } // Aspect-style tracking (simplified) public static MethodExecutionEvent start(String className, String methodName, Object[] args) { MethodExecutionEvent event = new MethodExecutionEvent(); event.begin(); event.className = className; event.methodName = methodName; if (args != null && args.length > 0) { event.arguments = java.util.Arrays.toString(args); } return event; } public void end(Object returnValue) { this.returnValue = String.valueOf(returnValue); this.executionTimeMs = getDuration().toMillis(); commit(); } public void endWithException(Throwable throwable) { this.exception = throwable.getClass().getName() + ": " + throwable.getMessage(); this.executionTimeMs = getDuration().toMillis(); commit(); } } // Usage with annotation processing public class TracedService { @TraceMethod(trackArguments = true) public String processData(String input, int count) { // Simulate processing try { Thread.sleep(ThreadLocalRandom.current().nextInt(5, 50)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if ("error".equals(input)) { throw new RuntimeException("Simulated error"); } return "Processed: " + input + " x" + count; } @TraceMethod(value = "calculateTotal", trackArguments = true) public double calculateTotal(double[] prices, double taxRate) { double subtotal = java.util.Arrays.stream(prices).sum(); return subtotal * (1 + taxRate); } } // Method interceptor (simplified) public class MethodTracingInterceptor { public static Object intercept(Method method, Object[] args, Object target) throws Throwable { TraceMethod annotation = method.getAnnotation(TraceMethod.class); if (annotation == null) { return method.invoke(target, args); } MethodExecutionEvent event = MethodExecutionEvent.start( target.getClass().getName(), annotation.value().isEmpty() ? method.getName() : annotation.value(), annotation.trackArguments() ? args : null ); try { Object result = method.invoke(target, args); event.end(result); return result; } catch (InvocationTargetException e) { event.endWithException(e.getCause()); throw e.getCause(); } } } 3. Advanced Event Types
Example 4: Periodic and Sampling Events
import jdk.jfr.*; import java.util.concurrent.atomic.*; @Name("com.company.SystemMetricsEvent") @Label("System Metrics") @Category({"Monitoring", "System"}) @Description("Periodic system metrics collection") @Period("1 s") // Emit every second public class SystemMetricsEvent extends Event { @Label("CPU Usage") @Description("Process CPU usage percentage") @Percentage private double cpuUsage; @Label("Memory Usage") @Description("Heap memory usage in MB") private double memoryUsageMB; @Label("Active Threads") private int activeThreads; @Label("GC Count") @Description("Total garbage collection count") private long gcCount; @Label("Request Rate") @Description("Requests per second") private double requestsPerSecond; // Getters and setters public double getCpuUsage() { return cpuUsage; } public void setCpuUsage(double cpuUsage) { this.cpuUsage = cpuUsage; } public double getMemoryUsageMB() { return memoryUsageMB; } public void setMemoryUsageMB(double memoryUsageMB) { this.memoryUsageMB = memoryUsageMB; } public int getActiveThreads() { return activeThreads; } public void setActiveThreads(int activeThreads) { this.activeThreads = activeThreads; } public long getGcCount() { return gcCount; } public void setGcCount(long gcCount) { this.gcCount = gcCount; } public double getRequestsPerSecond() { return requestsPerSecond; } public void setRequestsPerSecond(double requestsPerSecond) { this.requestsPerSecond = requestsPerSecond; } } @Name("com.company.BusinessMetricsEvent") @Label("Business Metrics") @Category({"Business", "Monitoring"}) @Description("Key business performance indicators") @Period("5 s") // Emit every 5 seconds public class BusinessMetricsEvent extends Event { @Label("Orders Processed") @Description("Total orders processed") private long ordersProcessed; @Label("Revenue") @Description("Total revenue in USD") private double revenue; @Label("Active Users") private int activeUsers; @Label("Error Rate") @Percentage private double errorRate; @Label("Average Response Time") @Timespan(Timespan.MILLISECONDS) private double avgResponseTime; // Getters and setters public long getOrdersProcessed() { return ordersProcessed; } public void setOrdersProcessed(long ordersProcessed) { this.ordersProcessed = ordersProcessed; } public double getRevenue() { return revenue; } public void setRevenue(double revenue) { this.revenue = revenue; } public int getActiveUsers() { return activeUsers; } public void setActiveUsers(int activeUsers) { this.activeUsers = activeUsers; } public double getErrorRate() { return errorRate; } public void setErrorRate(double errorRate) { this.errorRate = errorRate; } public double getAvgResponseTime() { return avgResponseTime; } public void setAvgResponseTime(double avgResponseTime) { this.avgResponseTime = avgResponseTime; } } // Metrics collector service public class MetricsCollector { private final AtomicLong ordersProcessed = new AtomicLong(0); private final AtomicLong totalRevenue = new AtomicLong(0); private final AtomicInteger activeUsers = new AtomicInteger(0); private final AtomicLong totalErrors = new AtomicLong(0); private final AtomicLong totalRequests = new AtomicLong(0); private final AtomicLong totalResponseTime = new AtomicLong(0); private volatile long lastGcCount = 0; public void recordOrder(double amount) { ordersProcessed.incrementAndGet(); totalRevenue.addAndGet((long) (amount * 100)); // Store as cents } public void recordRequest(long responseTimeMs, boolean error) { totalRequests.incrementAndGet(); totalResponseTime.addAndGet(responseTimeMs); if (error) { totalErrors.incrementAndGet(); } } public void userLoggedIn() { activeUsers.incrementAndGet(); } public void userLoggedOut() { activeUsers.decrementAndGet(); } // Called periodically to emit events public void emitSystemMetrics() { SystemMetricsEvent event = new SystemMetricsEvent(); // Simulate metric collection event.cpuUsage = Math.random() * 100; event.memoryUsageMB = Runtime.getRuntime().totalMemory() / (1024.0 * 1024.0); event.activeThreads = Thread.activeCount(); // Calculate requests per second (simplified) event.requestsPerSecond = totalRequests.get() / 60.0; // Per minute for demo event.commit(); } public void emitBusinessMetrics() { BusinessMetricsEvent event = new BusinessMetricsEvent(); event.ordersProcessed = ordersProcessed.get(); event.revenue = totalRevenue.get() / 100.0; // Convert cents to dollars event.activeUsers = activeUsers.get(); long totalReqs = totalRequests.get(); event.errorRate = totalReqs > 0 ? (totalErrors.get() * 100.0 / totalReqs) : 0; event.avgResponseTime = totalReqs > 0 ? (totalResponseTime.get() / (double) totalReqs) : 0; event.commit(); } } 4. Programmatic Event Control
Example 5: Dynamic Event Control
import jdk.jfr.*; import java.util.*; import java.util.concurrent.*; public class DynamicEventControl { private final Map<String, Boolean> eventEnabled = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); @Name("com.company.DynamicEvent") @Label("Dynamically Controlled Event") @Category("Custom") public class DynamicEvent extends Event { @Label("Event Type") private String eventType; @Label("Payload") private String payload; @Label("Importance") @Description("Event importance level") private String importance; public String getEventType() { return eventType; } public void setEventType(String eventType) { this.eventType = eventType; } public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } public String getImportance() { return importance; } public void setImportance(String importance) { this.importance = importance; } // Dynamic enablement check public boolean shouldRecord() { return eventEnabled.getOrDefault(eventType, true); } } // Event factory with dynamic control public class DynamicEventFactory { public void recordEvent(String eventType, String payload, String importance) { DynamicEvent event = new DynamicEvent(); // Check if this event type is enabled if (!event.shouldRecord()) { return; } event.begin(); event.eventType = eventType; event.payload = payload; event.importance = importance; event.commit(); } public void enableEventType(String eventType) { eventEnabled.put(eventType, true); System.out.println("Enabled event type: " + eventType); } public void disableEventType(String eventType) { eventEnabled.put(eventType, false); System.out.println("Disabled event type: " + eventType); } public void setEventThreshold(String eventType, long thresholdMs) { // Dynamic threshold configuration // This would typically use JFR's configuration API System.out.println("Set threshold for " + eventType + ": " + thresholdMs + "ms"); } } // Programmatic JFR control public class JFRController { public void startRecording(String name, Duration duration) { try { Recording recording = new Recording(); recording.setName(name); recording.setDestination(Paths.get(name + ".jfr")); recording.setDuration(duration); // Enable specific event settings recording.enable("com.company.OrderProcessingEvent"); recording.enable("com.company.DatabaseQueryEvent").withThreshold(Duration.ofMillis(10)); recording.enable("com.company.MethodExecutionEvent").withStackTrace(); recording.start(); System.out.println("Started recording: " + name); } catch (Exception e) { System.err.println("Failed to start recording: " + e.getMessage()); } } public void configureCustomEvents() { // Programmatically configure event settings Configuration config = Configuration.getConfiguration("default"); // Modify event settings (simplified - actual API may differ) System.out.println("Configuring custom events..."); // Enable custom events with specific thresholds enableEventWithThreshold("com.company.OrderProcessingEvent", Duration.ofMillis(5)); enableEventWithThreshold("com.company.DatabaseQueryEvent", Duration.ofMillis(10)); } private void enableEventWithThreshold(String eventName, Duration threshold) { // Implementation would use JFR configuration API System.out.println("Enabled " + eventName + " with threshold: " + threshold); } public List<Recording> getActiveRecordings() { return FlightRecorder.getFlightRecorder().getRecordings(); } public void dumpRecordingInfo() { FlightRecorder fr = FlightRecorder.getFlightRecorder(); System.out.println("Available Events:"); for (EventType type : fr.getEventTypes()) { System.out.printf(" %s - %s%n", type.getName(), type.getLabel()); } System.out.println("\nActive Recordings:"); for (Recording recording : fr.getRecordings()) { System.out.printf(" %s - %s%n", recording.getName(), recording.getState()); } } } 5. Real-World Use Cases
Example 6: Complete Application Monitoring
import jdk.jfr.*; import java.util.*; import java.util.concurrent.*; public class ApplicationMonitoring { // Cache operation event @Name("com.company.CacheEvent") @Label("Cache Operation") @Category({"Performance", "Caching"}) public class CacheEvent extends Event { @Label("Cache Name") private String cacheName; @Label("Operation") @Description("Cache operation type") private String operation; // GET, PUT, REMOVE, etc. @Label("Key") private String key; @Label("Hit") private boolean hit; @Label("Execution Time") @Timespan(Timespan.NANOSECONDS) private long executionTimeNs; @Label("Cache Size") private int cacheSize; // Getters and setters public String getCacheName() { return cacheName; } public void setCacheName(String cacheName) { this.cacheName = cacheName; } public String getOperation() { return operation; } public void setOperation(String operation) { this.operation = operation; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public boolean isHit() { return hit; } public void setHit(boolean hit) { this.hit = hit; } public long getExecutionTimeNs() { return executionTimeNs; } public void setExecutionTimeNs(long executionTimeNs) { this.executionTimeNs = executionTimeNs; } public int getCacheSize() { return cacheSize; } public void setCacheSize(int cacheSize) { this.cacheSize = cacheSize; } } // HTTP request event @Name("com.company.HttpRequestEvent") @Label("HTTP Request") @Category({"Web", "Performance"}) public class HttpRequestEvent extends Event { @Label("Request ID") private String requestId; @Label("HTTP Method") private String method; @Label("URL") private String url; @Label("Status Code") private int statusCode; @Label("Response Time") @Timespan(Timespan.MILLISECONDS) private long responseTimeMs; @Label("Request Size") @DataAmount private long requestSize; @Label("Response Size") @DataAmount private long responseSize; @Label("Client IP") private String clientIp; @Label("User Agent") private String userAgent; // Getters and setters public String getRequestId() { return requestId; } public void setRequestId(String requestId) { this.requestId = requestId; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public long getResponseTimeMs() { return responseTimeMs; } public void setResponseTimeMs(long responseTimeMs) { this.responseTimeMs = responseTimeMs; } public long getRequestSize() { return requestSize; } public void setRequestSize(long requestSize) { this.requestSize = requestSize; } public long getResponseSize() { return responseSize; } public void setResponseSize(long responseSize) { this.responseSize = responseSize; } public String getClientIp() { return clientIp; } public void setClientIp(String clientIp) { this.clientIp = clientIp; } public String getUserAgent() { return userAgent; } public void setUserAgent(String userAgent) { this.userAgent = userAgent; } } // Batch processing event @Name("com.company.BatchProcessingEvent") @Label("Batch Processing") @Category({"Batch", "Performance"}) public class BatchProcessingEvent extends Event { @Label("Batch ID") private String batchId; @Label("Batch Size") private int batchSize; @Label("Processed Items") private int processedItems; @Label("Failed Items") private int failedItems; @Label("Total Time") @Timespan(Timespan.MILLISECONDS) private long totalTimeMs; @Label("Throughput") @Description("Items processed per second") private double throughput; @Label("Success Rate") @Percentage private double successRate; // Getters and setters public String getBatchId() { return batchId; } public void setBatchId(String batchId) { this.batchId = batchId; } public int getBatchSize() { return batchSize; } public void setBatchSize(int batchSize) { this.batchSize = batchSize; } public int getProcessedItems() { return processedItems; } public void setProcessedItems(int processedItems) { this.processedItems = processedItems; } public int getFailedItems() { return failedItems; } public void setFailedItems(int failedItems) { this.failedItems = failedItems; } public long getTotalTimeMs() { return totalTimeMs; } public void setTotalTimeMs(long totalTimeMs) { this.totalTimeMs = totalTimeMs; } public double getThroughput() { return throughput; } public void setThroughput(double throughput) { this.throughput = throughput; } public double getSuccessRate() { return successRate; } public void setSuccessRate(double successRate) { this.successRate = successRate; } } } // Comprehensive monitoring service public class MonitoringService { private final DynamicEventControl eventControl = new DynamicEventControl(); private final MetricsCollector metricsCollector = new MetricsCollector(); private final JFRController jfrController = new JFRController(); public void initializeMonitoring() { // Start continuous recording jfrController.startRecording("Application-Monitoring", Duration.ofHours(1)); // Configure event thresholds jfrController.configureCustomEvents(); // Start periodic metrics emission ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { metricsCollector.emitSystemMetrics(); metricsCollector.emitBusinessMetrics(); }, 0, 5, TimeUnit.SECONDS); System.out.println("Application monitoring initialized"); } public void recordCacheOperation(String cacheName, String operation, String key, boolean hit, long durationNs, int cacheSize) { ApplicationMonitoring.CacheEvent event = new ApplicationMonitoring.CacheEvent(); event.begin(); event.cacheName = cacheName; event.operation = operation; event.key = key; event.hit = hit; event.executionTimeNs = durationNs; event.cacheSize = cacheSize; event.commit(); } public void recordHttpRequest(String requestId, String method, String url, int statusCode, long responseTimeMs, long requestSize, long responseSize, String clientIp, String userAgent) { ApplicationMonitoring.HttpRequestEvent event = new ApplicationMonitoring.HttpRequestEvent(); event.begin(); event.requestId = requestId; event.method = method; event.url = url; event.statusCode = statusCode; event.responseTimeMs = responseTimeMs; event.requestSize = requestSize; event.responseSize = responseSize; event.clientIp = clientIp; event.userAgent = userAgent; event.commit(); } public void recordBatchProcessing(String batchId, int batchSize, int processedItems, int failedItems, long totalTimeMs) { ApplicationMonitoring.BatchProcessingEvent event = new ApplicationMonitoring.BatchProcessingEvent(); event.begin(); event.batchId = batchId; event.batchSize = batchSize; event.processedItems = processedItems; event.failedItems = failedItems; event.totalTimeMs = totalTimeMs; event.throughput = (processedItems * 1000.0) / totalTimeMs; event.successRate = (processedItems - failedItems) * 100.0 / processedItems; event.commit(); } } 6. Event Processing and Analysis
Example 7: Event Analysis and Reporting
import jdk.jfr.consumer.*; import java.nio.file.*; import java.util.*; import java.util.stream.*; public class JFREventAnalyzer { public void analyzeRecording(String recordingPath) throws Exception { Path jfrFile = Paths.get(recordingPath); try (RecordingFile recording = new RecordingFile(jfrFile)) { Map<String, List<RecordedEvent>> eventsByType = new HashMap<>(); Map<String, EventStatistics> statistics = new HashMap<>(); while (recording.hasMoreEvents()) { RecordedEvent event = recording.readEvent(); String eventType = event.getEventType().getName(); eventsByType.computeIfAbsent(eventType, k -> new ArrayList<>()).add(event); updateStatistics(statistics, event); } generateAnalysisReport(eventsByType, statistics); } } private void updateStatistics(Map<String, EventStatistics> statistics, RecordedEvent event) { String eventType = event.getEventType().getName(); EventStatistics stats = statistics.computeIfAbsent(eventType, k -> new EventStatistics()); stats.count++; // Update duration statistics if available if (event.hasField("duration")) { Duration duration = event.getDuration(); long durationMs = duration.toMillis(); stats.totalDurationMs += durationMs; stats.minDurationMs = Math.min(stats.minDurationMs, durationMs); stats.maxDurationMs = Math.max(stats.maxDurationMs, durationMs); } // Update timestamp range Instant eventTime = event.getStartTime(); if (stats.firstEvent == null || eventTime.isBefore(stats.firstEvent)) { stats.firstEvent = eventTime; } if (stats.lastEvent == null || eventTime.isAfter(stats.lastEvent)) { stats.lastEvent = eventTime; } } private void generateAnalysisReport(Map<String, List<RecordedEvent>> eventsByType, Map<String, EventStatistics> statistics) { System.out.println("📊 JFR RECORDING ANALYSIS REPORT"); System.out.println("=".repeat(80)); // Event type summary System.out.println("\n📈 EVENT TYPE SUMMARY:"); statistics.entrySet().stream() .sorted((a, b) -> Long.compare(b.getValue().count, a.getValue().count)) .forEach(entry -> { EventStatistics stats = entry.getValue(); System.out.printf(" %-40s %6d events", entry.getKey(), stats.count); if (stats.totalDurationMs > 0) { double avgDuration = stats.totalDurationMs / (double) stats.count; System.out.printf(" | Avg: %6.1f ms | Min: %4d ms | Max: %5d ms", avgDuration, stats.minDurationMs, stats.maxDurationMs); } System.out.println(); }); // Custom event analysis analyzeCustomEvents(eventsByType); } private void analyzeCustomEvents(Map<String, List<RecordedEvent>> eventsByType) { System.out.println("\n🔍 CUSTOM EVENT ANALYSIS:"); // Analyze order processing events List<RecordedEvent> orderEvents = eventsByType.get("com.company.OrderProcessingEvent"); if (orderEvents != null) { analyzeOrderEvents(orderEvents); } // Analyze database events List<RecordedEvent> dbEvents = eventsByType.get("com.company.DatabaseQueryEvent"); if (dbEvents != null) { analyzeDatabaseEvents(dbEvents); } } private void analyzeOrderEvents(List<RecordedEvent> events) { long successCount = events.stream() .filter(e -> e.getBoolean("success")) .count(); long failCount = events.size() - successCount; double avgAmount = events.stream() .mapToDouble(e -> e.getDouble("amount")) .average() .orElse(0); double avgProcessingTime = events.stream() .mapToLong(e -> e.getLong("processingTimeMs")) .average() .orElse(0); System.out.println(" Order Processing Events:"); System.out.printf(" Total: %d | Success: %d | Failed: %d | Success Rate: %.1f%%%n", events.size(), successCount, failCount, (successCount * 100.0 / events.size())); System.out.printf(" Average Amount: $%.2f | Average Processing Time: %.1f ms%n", avgAmount, avgProcessingTime); } private void analyzeDatabaseEvents(List<RecordedEvent> events) { long successCount = events.stream() .filter(e -> e.getBoolean("success")) .count(); double avgExecutionTime = events.stream() .mapToLong(e -> e.getLong("executionTimeMs")) .average() .orElse(0); Map<String, Long> queriesByTable = events.stream() .collect(Collectors.groupingBy( e -> e.getString("tableName"), Collectors.counting() )); System.out.println(" Database Query Events:"); System.out.printf(" Total: %d | Success: %d | Success Rate: %.1f%%%n", events.size(), successCount, (successCount * 100.0 / events.size())); System.out.printf(" Average Execution Time: %.1f ms%n", avgExecutionTime); System.out.println(" Queries by Table:"); queriesByTable.entrySet().stream() .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) .forEach(entry -> { System.out.printf(" %-15s: %d queries%n", entry.getKey(), entry.getValue()); }); } static class EventStatistics { long count = 0; long totalDurationMs = 0; long minDurationMs = Long.MAX_VALUE; long maxDurationMs = Long.MIN_VALUE; Instant firstEvent; Instant lastEvent; } // Real-time event processing public void startRealTimeAnalysis() { try { FlightRecorder.addHandler(this::processEvent); System.out.println("Started real-time JFR event processing"); } catch (Exception e) { System.err.println("Failed to start real-time analysis: " + e.getMessage()); } } private void processEvent(RecordedEvent event) { String eventType = event.getEventType().getName(); // Process specific event types in real-time switch (eventType) { case "com.company.OrderProcessingEvent": processOrderEvent(event); break; case "com.company.DatabaseQueryEvent": processDatabaseEvent(event); break; case "jdk.GarbageCollection": processGCEvent(event); break; } } private void processOrderEvent(RecordedEvent event) { boolean success = event.getBoolean("success"); double amount = event.getDouble("amount"); long processingTime = event.getLong("processingTimeMs"); // Real-time alerting for slow orders if (processingTime > 1000) { // 1 second threshold System.out.printf("🚨 SLOW ORDER: %s took %d ms%n", event.getString("orderId"), processingTime); } // Real-time alerting for failed orders if (!success) { System.out.printf("❌ FAILED ORDER: %s - %s%n", event.getString("orderId"), event.getString("failureReason")); } } private void processDatabaseEvent(RecordedEvent event) { long executionTime = event.getLong("executionTimeMs"); boolean success = event.getBoolean("success"); // Alert for slow queries if (executionTime > 100) { // 100ms threshold System.out.printf("🐌 SLOW QUERY: %s took %d ms%n", event.getString("queryId"), executionTime); } // Alert for query failures if (!success) { System.out.printf("💥 QUERY FAILED: %s - %s%n", event.getString("queryId"), event.getString("errorMessage")); } } private void processGCEvent(RecordedEvent event) { long duration = event.getDuration().toMillis(); // Alert for long GC pauses if (duration > 100) { // 100ms threshold System.out.printf("🗑️ LONG GC PAUSE: %d ms - %s%n", duration, event.getString("gcName")); } } } Key Benefits and Best Practices
Benefits:
- Low Overhead: JFR events have minimal performance impact
- Integration: Works with existing JVM monitoring tools
- Rich Metadata: Stack traces, timing, custom fields
- Production Safe: Designed for use in production environments
Best Practices:
- Use Meaningful Event Names: Include package names for organization
- Set Appropriate Thresholds: Avoid recording too many events
- Include Stack Traces Sparingly: They add overhead
- Use Periodic Events for Metrics: Instead of recording every occurrence
- Test Event Overhead: Measure performance impact in your environment
Running with Custom Events:
# Enable JFR and custom events java -XX:+FlightRecorder -XX:StartFlightRecording=filename=recording.jfr,duration=60s \ -cp your-app.jar YourMainClass # With custom settings java -XX:+FlightRecorder \ -XX:StartFlightRecording=settings=profile,filename=recording.jfr \ -cp your-app.jar YourMainClass
This comprehensive guide shows how to create, configure, and use custom JFR events to gain deep insights into your Java application's behavior and performance.