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.