Java Flight Recorder (JFR) Support
Java Flight Recorder (JFR) Support
Introduction
Java Flight Recorder (JFR) is a low-overhead profiling and diagnostics framework built into the JDK (11+). Unlike traditional profiling tools, JFR has minimal performance impact (typically <1%) making it suitable for continuous use in production environments.
Useful JFR Resources:
- JDK Flight Recorder Guide - Official Oracle documentation
- Flight Recorder API Programmer's Guide - Creating custom JFR events in Java
- JFR Event Streaming - Real-time event processing (JDK 14+)
- JDK Mission Control - Visual JFR analysis tool
- OpenJDK JEP 328 - Flight Recorder specification and implementation details
- Using JFR to Monitor Applications - Practical monitoring guide
- JFR Command Reference - Command-line tool documentation
Lucee 8.0+ adds native support for emitting custom JFR events from your CFML code, allowing you to:
- Track business-critical operations alongside JVM metrics
- Correlate application behavior with system performance
- Create custom duration and instant events
- Analyze recordings with standard JFR tools (JDK Mission Control, etc.)
Quick Start
Emitting Events
// Instant event - marks a point in time
jfrEmit("business", "Order Processed", {
orderId: 12345,
amount: 99.99,
customerId: 456
});
// Duration event - measures time
var eventId = jfrBegin("database", "Complex Query", {
table: "orders",
filters: "status=pending"
});
// ... perform operation ...
jfrCommit(eventId, {
rowCount: 250,
cached: false
});
Using Tags
// Add JFR events to cftrace
<cftrace
type="Information"
category="business"
text="Processing payment"
jfr="true">
// Measure execution time with cftimer
<cftimer label="Heavy Operation" jfr="true">
<!--- Your code here --->
</cftimer>
Starting a Recording
JFR recordings can be started in several ways:
1. Programmatically (BIF)
// Start recording
recordingId = jfrStartRecording({
name: "Production Monitoring",
maxAge: 3600, // Keep last hour of data
dumpOnExit: true
});
// Emit your events
jfrEmit("app", "User Login", { userId: 123 });
// Stop and save
jfrStopRecording(recordingId, "/var/logs/myapp.jfr");
2. JVM Startup Arguments
java -XX:StartFlightRecording=filename=recording.jfr,duration=60s,settings=profile \
-jar lucee.jar
3. External Tools (jcmd)
# Start recording
jcmd <pid> JFR.start name=myrecording
# Stop and save
jcmd <pid> JFR.stop name=myrecording filename=recording.jfr
Available BIFs
jfrAvailable()
Returns true if JFR is available in the current JVM (JDK 11+) and the internal JDK modules are exported/open and available to Lucee.
if (jfrAvailable()) {
// JFR functionality available
}
jfrEnabled()
Returns true if JFR is available AND at least one recording is active.
if (jfrEnabled()) {
jfrEmit("app", "Feature Used");
}
jfrEmit(category, label [, data])
Emits an instant event (point in time).
Arguments:
category(string, required): Event category for groupinglabel(string, required): Event name/descriptiondata(struct, optional): Additional event data (JSON serialized)
// Simple event
jfrEmit("business", "Order Submitted");
// Event with data
jfrEmit("cache", "Cache Miss", {
key: "user:123",
reason: "expired"
});
jfrBegin(category, label [, data])
Starts a duration event. Returns an event ID.
Arguments:
category(string, required): Event categorylabel(string, required): Event namedata(struct, optional): Initial event data
Returns: String (event ID)
var eventId = jfrBegin("api", "External API Call", {
endpoint: "https://api.example.com/users",
method: "GET"
});
jfrCommit(eventId [, data])
Completes a duration event started with jfrBegin().
Arguments:
eventId(string, required): Event ID fromjfrBegin()data(struct, optional): Additional/final event data
try {
var eventId = jfrBegin("payment", "Process Payment");
// ... payment processing ...
jfrCommit(eventId, { success: true, amount: 99.99 });
}
catch (any e) {
jfrCommit(eventId, { success: false, error: e.message });
}
jfrStartRecording([options])
Starts a new JFR recording programmatically.
Arguments:
options(struct, optional): Recording configurationname(string): Recording namemaxAge(numeric): Max age in secondsmaxSize(numeric): Max size in bytesdumpOnExit(boolean): Dump on JVM exit
Returns: Numeric (recording ID)
recordingId = jfrStartRecording({
name: "Request Analysis",
maxAge: 600,
dumpOnExit: false
});
jfrStopRecording(recordingId [, destination])
Stops an active recording and optionally saves it.
Arguments:
recordingId(numeric, required): Recording ID fromjfrStartRecording()destination(string, optional): File path to save recording
Returns: String (confirmation message or file path)
jfrStopRecording(recordingId, "/var/logs/recording.jfr");
jfrAnalyze(jfrFile [, returnType, options])
Analyzes a JFR recording file and extracts events.
Arguments:
jfrFile(string, required): Path to .jfr filereturnType(string, optional): "array" (default) or "query"options(struct, optional): Filtering and output optionseventTypes(string/array): Filter by event type namescategory(string/array): Filter by categoriesstartTime(datetime): Only events after this timeendTime(datetime): Only events before this timeminDuration(numeric): Minimum duration in millisecondsmaxDuration(numeric): Maximum duration in millisecondsincludeStackTraces(boolean): Include stack traces (default: false)maxEvents(numeric): Limit resultssortBy(string): Sort by "duration", "startTime", or "eventType"sortOrder(string): "asc" or "desc"fields(string/array): Specific fields to include
Returns: Array of structs or Query
// Simple analysis
events = jfrAnalyze("/var/logs/recording.jfr");
// Filter and sort
events = jfrAnalyze("/var/logs/recording.jfr", "array", {
eventTypes: "lucee.runtime.jfr.CustomEvent",
minDuration: 100,
sortBy: "duration",
sortOrder: "desc",
maxEvents: 50
});
// As query for easy output
qry = jfrAnalyze("/var/logs/recording.jfr", "query", {
category: ["business", "payment"]
});
Tag Enhancements
cftrace with JFR
Add jfr="true" to emit trace information as JFR events:
<cftrace
type="Information"
category="business"
text="Processing order #order.id#"
var="order"
jfr="true">
This creates a JFR event with:
- Event type, category, and text
- Template path and line number
- Variable name and value (if specified)
cftimer with JFR
Add jfr="true" to emit timer duration as a JFR event:
<cftimer label="Database Query" jfr="true">
<cfquery name="qOrders">
SELECT * FROM orders WHERE status = 'pending'
</cfquery>
</cftimer>
This creates a JFR duration event with:
- Timer label
- Execution duration
- Success/failure status
- Template path and line number
Note: The timer will emit JFR events even if exceptions occur (using try/catch/finally internally).
Use Cases
Production Monitoring
Track key business metrics with minimal overhead:
function processPayment(amount, customerId) {
var eventId = jfrBegin("payment", "Process Payment", {
amount: amount,
customerId: customerId
});
try {
// Payment processing logic
var result = paymentGateway.charge(amount);
jfrCommit(eventId, {
success: true,
transactionId: result.id,
processingTime: result.time
});
return result;
}
catch (any e) {
jfrCommit(eventId, {
success: false,
error: e.message,
errorType: e.type
});
rethrow;
}
}
Performance Analysis
Identify slow operations:
// During request
var eventId = jfrBegin("cache", "Redis Get");
var data = redisClient.get(key);
jfrCommit(eventId, { hit: !isNull(data) });
// Later, analyze recording
events = jfrAnalyze("production.jfr", "array", {
category: "cache",
minDuration: 50, // Slower than 50ms
sortBy: "duration",
sortOrder: "desc"
});
// Find cache operations > 50ms
for (event in events) {
writeOutput("Slow cache operation: #event.duration#ms<br>");
}
Request Correlation
Link application events with JVM metrics:
// At request start
var requestId = createUUID();
request.jfrEventId = jfrBegin("http", "Request", {
requestId: requestId,
method: cgi.request_method,
path: cgi.script_name,
remoteAddr: cgi.remote_addr
});
// At request end
jfrCommit(request.jfrEventId, {
statusCode: request.statusCode ?: 200,
responseTime: getTickCount() - request.startTime
});
A/B Testing Metrics
Track feature usage in production:
function showFeature() {
var variant = request.abTest.variant; // "A" or "B"
jfrEmit("feature", "Feature Shown", {
featureName: "new_checkout",
variant: variant,
userId: session.userId
});
}
// Analyze later
events = jfrAnalyze("production.jfr", "query", {
eventTypes: "lucee.runtime.jfr.CustomEvent",
category: "feature"
});
// Group by variant
cfquery(name="variantCounts" dbtype="query") {
writeOutput("
SELECT JSON_VALUE(fields, '$.variant') as variant,
COUNT(*) as count
FROM events
GROUP BY JSON_VALUE(fields, '$.variant')
");
}
Analyzing Recordings
Using jfrAnalyze() BIF
// Get all Lucee custom events
events = jfrAnalyze("recording.jfr", "array", {
eventTypes: "lucee.runtime.jfr.CustomEvent"
});
// Find slow operations
slowOps = jfrAnalyze("recording.jfr", "array", {
minDuration: 1000, // > 1 second
sortBy: "duration",
sortOrder: "desc",
maxEvents: 10
});
// Time range analysis
eventsInRange = jfrAnalyze("recording.jfr", "array", {
startTime: dateAdd("h", -1, now()),
endTime: now()
});
Using JDK Mission Control
- Open JDK Mission Control
- Load your .jfr file
- Navigate to "Event Browser"
- Look for "Lucee" category to find custom events
- View event details, stack traces, and correlate with JVM metrics
Using jfr Command Line
# Print all Lucee events
jfr print --events lucee.runtime.jfr.CustomEvent recording.jfr
# Summary of event types
jfr summary recording.jfr
# Convert to JSON for processing
jfr print --json recording.jfr > events.json
Performance Impact
JFR is designed for production use with minimal overhead:
- Disabled (no recording): No performance impact
- Recording enabled: Typically <1% overhead
- With stack traces: ~2% overhead
- Events only emitted if recording active: Your
jfrEmit()calls have near-zero cost when no recording is running
Best Practices:
- Check
jfrEnabled()before emitting high-frequency events - Use categories to filter events during analysis
- Limit stack trace collection to when needed
- Use
maxAgeto prevent unbounded recording growth
Comparison with ExecutionLog
| Feature | JFR | ExecutionLog |
|---|---|---|
| Overhead | <1% | 10-50% |
| Production Safe | ✅ Yes | ❌ No |
| Custom Events | ✅ Yes | ❌ No |
| JVM Metrics | ✅ Yes | ❌ No |
| Granularity | Custom | Every statement |
| Standard Tools | ✅ Yes | ❌ No |
| Use Case | Production monitoring | Deep development debugging |
Examples
Complete Monitoring Example
component {
function onRequestStart() {
// Start recording if needed
if (jfrAvailable() && !jfrEnabled()) {
application.recordingId = jfrStartRecording({
name: "Production",
maxAge: 3600,
dumpOnExit: true
});
}
// Track request
request.jfrRequestId = jfrBegin("http", "Request", {
method: cgi.request_method,
path: cgi.script_name,
userAgent: cgi.http_user_agent
});
}
function onRequestEnd() {
if (structKeyExists(request, "jfrRequestId")) {
jfrCommit(request.jfrRequestId, {
statusCode: request.statusCode ?: 200,
userAuthenticated: structKeyExists(session, "userId")
});
}
}
function onError(exception) {
jfrEmit("error", "Unhandled Exception", {
type: exception.type,
message: exception.message,
template: exception.tagContext[1].template,
line: exception.tagContext[1].line
});
}
}
Database Query Tracking
function queryWithTracking(sql, params={}) {
var eventId = jfrBegin("database", "Query", {
sql: left(sql, 100) // First 100 chars
});
var startTime = getTickCount();
try {
var qry = queryExecute(sql, params);
jfrCommit(eventId, {
rowCount: qry.recordCount,
cached: qry.cached,
executionTime: getTickCount() - startTime
});
return qry;
}
catch (any e) {
jfrCommit(eventId, {
error: e.message,
executionTime: getTickCount() - startTime
});
rethrow;
}
}
Troubleshooting
JFR Not Available
if (!jfrAvailable()) {
// Running on JDK 8 or JFR classes not accessible
// JFR requires JDK 11+
}
No Events in Recording
- Ensure a recording is active:
jfrEnabled()returnstrue - Check event categories match your analysis filters
- Verify events were emitted during the recording period
- Check recording
maxAgehasn't expired events
High Memory Usage
- Reduce
maxAgeto limit recording buffer - Filter events by category during recording
- Use
maxSizeto cap recording size - Disable stack traces if not needed