DEV Community

Machine coding Master
Machine coding Master

Posted on

Span-to-Profile Correlation: Link OpenTelemetry Spans to JFR Samples to Kill P99 Outliers

Span-to-Profile Correlation: Link OpenTelemetry Spans to JFR Samples to Kill P99 Outliers

Aggregated flamegraphs are useless when an isolated P99 request stalls on a single thread due to allocation bursts or lock contention. Staring at 30-minute averaged profiles while your latency SLAs burn is a rookie mistake you need to stop making.

Why Most Developers Get This Wrong

  • Treating profiling as aggregate-only: Averaging millions of thread samples completely washes out the transient 400ms lock inflation hit by 0.01% of requests.
  • Timestamp alignment guessing: Grepping APM timestamps against JFR recordings fails due to clock drift, thread re-use, and non-deterministic OS thread scheduling.
  • Ignoring JVM-level context propagation: Emitting spans without linking the active TraceId and SpanId into the thread state makes thread-level JFR events impossible to query deterministically.

The Right Way

Bridge your OpenTelemetry tracing pipeline directly to JVM flight recordings using the OpenTelemetry Profiling Data Model to slice samples strictly within an active span's lifespan.

  • Use the OpenTelemetry Java instrumentation agent with continuous profiling extensions to bind SpanContext into JFR thread-local state.
  • Isolate tail latency by filtering jdk.ExecutionSample, jdk.JavaMonitorEnter, and jdk.ObjectAllocationInNewTLAB events strictly by the targeted trace_id.
  • Export via the OTel continuous profiling protocol directly into backends supporting trace-to-profile linking to render an isolated flamegraph representing only that P99 span.

Show Me The Code

Tag your JFR stream directly with the active span context at critical execution boundaries:

@Category("OpenTelemetry")
@StackTrace(false)
public class SpanMarkerEvent extends Event {
    @Label("Trace ID") public String traceId;
    @Label("Span ID") public String spanId;
}

// In your high-impact path:
Span span = tracer.spanBuilder("order-reconciliation").startSpan();
try (Scope scope = span.makeCurrent()) {
    SpanMarkerEvent event = new SpanMarkerEvent();
    event.traceId = span.getSpanContext().getTraceId();
    event.spanId = span.getSpanContext().getSpanId();
    event.begin();
    processP99Workload();
    event.commit();
} finally {
    span.end();
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Aggregate flamegraphs hide outliers; span-correlated JFR profiles reveal the exact method frames executing during latency spikes.
  • The OpenTelemetry Profiling Data Model turns thread dumps and execution samples into queryable trace attributes.
  • Stop guessing via timestamps—tag JVM execution events with trace_id at the thread boundary.

Heads up: if you want to see these patterns applied to real interview problems, javalld.com has full machine coding solutions with traces.

Top comments (0)