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.