Tracing in Java: A Practical Guide to Observability

Tracing in Java: A Practical Guide to Observability

Most advice about tracing in Java starts with the wrong assumption, that every slow request deserves a trace. That's how teams end up instrumenting everything, paying for noisy spans, and still missing the problem because the bottleneck lived in a pool, a lock, or a CPU hotspot, not in request routing. Tracing is powerful, but it's a narrowing tool first, a diagnosis tool second.

The better posture is blunt. Start with the symptom, choose the signal that can answer it fastest, and only then decide whether tracing belongs in the path. In Java, that usually means traces for cross-service flow, metrics for system health and saturation, and profiling for code-level stalls.

Table of Contents

Why Tracing Is Not Always the Answer

Tracing helps most when the question is about request flow. If a checkout is slow because it crosses payment, inventory, and notification services, a single trace shows where time accumulated and how failures propagated across boundaries. That's exactly where a trace earns its keep, because logs alone don't reconstruct the path and metrics alone don't tell you which hop stalled.

Start with the symptom, not the tool

The wrong habit is to reach for tracing whenever an alert fires. If the symptom looks like connection starvation, lock contention, CPU saturation, or virtual-thread pinning, traces may only point at the request that got unlucky, not the resource that was exhausted. In those cases, metrics show the pressure on pools and schedulers, and profiling shows the code path that burned cycles.

Practical rule: use tracing when the failure crosses service boundaries, not when the likely root cause sits inside one JVM.

A good decision frame is simple. If the problem is “where did this request go,” tracing is the right first pass. If the problem is “why is this JVM slow under load,” metrics and profiling usually answer faster.

The distinction matters because Java tracing has evolved into a core runtime capability, not an optional add-on. IBM's J9 JVM documentation describes trace data as built into the platform, buffered in memory, and available in human-readable or compressed binary formats, while OpenJDK's JEP 520 frames tracing as a way to identify bottlenecks and root causes with exact invocation counts and average execution times in JFR IBM Java tracing documentation. That shift explains why tracing is now part of the observability baseline in modern JVM estates, but it doesn't make it the right answer for every incident.

Where tracing misleads

Tracing can also create confidence where none should exist. A neat waterfall diagram can hide the issue if the JVM is blocking on a shared resource before the span ever starts, or if async handoffs break context propagation. Traces are only as useful as the boundaries they can observe.

The other common failure mode is over-instrumentation. More spans don't always mean more insight. They can mean more noise, more storage, and more decisions during sampling, especially in services that already emit a lot of telemetry. For that reason, tracing is best treated like a laser, not floodlighting.

The internal monitoring context matters too. Teams that already use server and infrastructure dashboards often find traces most useful after an alert has already narrowed the blast radius, which is why a broader monitoring layer such as the monitoring of servers overview belongs in the operational stack even when tracing is available. Tracing is one lens, not the whole observability system.

Understanding Traces, Spans, and Context Propagation

A trace is the end-to-end record of a request. A span is one unit of work inside that request, like an HTTP handler, a database query, or a call to another service trace and span fundamentals. In distributed Java systems, the useful question is rarely “did the app work,” it's “which hop consumed the time.”

A diagram explaining traces, spans, and context propagation as the core components of distributed system observability.

Read a trace as a request story

Take a payment request moving through a payment service, inventory service, and notification service. The trace is the full journey, and each service contributes one or more spans. A parent span may cover the inbound HTTP request, while child spans mark downstream calls, SQL execution, or message publishing.

That structure matters because latency rarely lives in one place. A trace can show that the payment service was fast, the inventory service waited on a database query, and the notification service finished late because it picked up the request only after the others completed. The shape of the waterfall matters more than the raw duration of any single span.

A clean trace is not just a timeline. It is a causal map.

Context propagation is what makes the map continuous

Trace continuity depends on context propagation. The service that receives a request must carry the trace context forward so the next hop can attach its spans to the same trace ID. In HTTP-based systems that is usually straightforward, but async boundaries are where teams get bitten.

Message queues, executor handoffs, and thread switches can drop context if the application doesn't preserve it. That's why trace IDs sometimes disappear from logs or why a downstream call appears detached from the parent request. In practice, the issue is rarely the tracing backend. It's usually the application code or framework integration.

A trace visualization becomes readable once the relationships are clear:

  • Parent span: the broader unit of work that owns the request.
  • Child span: a nested operation, like JDBC execution or a remote API call.
  • Attributes: labels that carry useful context, such as route, method, or SQL operation.
  • Trace ID: the shared identifier that ties the whole story together.

The point isn't to add detail for its own sake. It's to answer a specific question quickly. When a span tree is healthy, the slow node usually stands out immediately, and the service owner can jump straight into the logs or metrics tied to that node.

Choosing the Right Java Tracing Library

Library choice is mostly about operational fit, not ideology. OpenTelemetry has become the vendor-neutral default for new work, but older estates still run on Brave/Zipkin or Jaeger clients because the tooling, dashboards, and sampling assumptions are already baked in. The right choice depends on how much migration risk the team can absorb today and how much portability it will need later.

The Arch guide to performance monitoring is useful background because it treats tracing as one part of a wider monitoring strategy, not as a standalone product decision. For teams comparing vendors and self-managed stacks side by side, a broader view like this overview of DevOps monitoring tools helps keep the decision tied to operations, not just API shape.

Compare the options by production constraints

Feature OpenTelemetry Brave/Zipkin Jaeger
Auto-instrumentation coverage Broad, especially with the Java agent Strong for established Zipkin setups Solid in Jaeger-centered stacks
Manual instrumentation API Modern and vendor-neutral Mature, familiar to older Zipkin users Works well inside Jaeger ecosystems
Exporter flexibility High, built for multiple backends Usually centered on Zipkin-compatible flows Best when Jaeger is the target
Ecosystem momentum Strongest for new projects Best for legacy continuity Good for teams already standardized on Jaeger
Legacy JVM fit Good, but version alignment still matters Often attractive for older services already in production Practical when the backend is already settled

OpenTelemetry is the default recommendation for new Java services because it avoids backend lock-in and fits better with logs, metrics, and traces in the same instrumentation model. Brave is still a sensible choice for teams with a stable Zipkin investment and tight sampling budgets, especially when replatforming would add more risk than value. Jaeger client libraries remain reasonable if the team is already committed to Jaeger and wants to keep the instrumentation path direct.

The trade-off is operational, not philosophical. OpenTelemetry gives the cleanest long-term path, but it still needs library alignment and export plumbing that can create friction in older services. Brave and Jaeger can be simpler inside a fixed stack, which matters when the service is already stable and the tracing layer should stay quiet.

Mixed estates need a migration boundary

Mixed architectures are common. Some services may already emit Brave spans, while newer Spring Boot services use the OpenTelemetry Java agent. That does not have to be a blocker, but it does require discipline around trace IDs, sampling policy, and where data is exported.

The cleanest migrations avoid a big-bang rewrite. New services can standardize on OpenTelemetry while older services continue on their current stack until they are touched for other reasons. That path reduces churn and keeps the observability model coherent enough to operate.

A practical boundary helps more than a perfect standard. If the backend can ingest both formats, teams can move service by service instead of forcing a platform-wide rewrite that competes with feature delivery and incident work. That is usually the difference between a tracing program that spreads and one that gets stalled in review.

Migration works best when the backend is treated as the contract and the libraries are treated as implementation details.

Setting Up Tracing in a Spring Boot Application

Spring Boot is usually the fastest place to get tracing right because the framework already understands HTTP, JDBC, and common context propagation patterns. The simplest production path is the OpenTelemetry Java agent attached at startup, because it gives immediate coverage with no application code changes. That said, the agent still needs to match the libraries in the service well enough to avoid hard-to-diagnose bytecode issues, as Spring's own guidance notes in its OpenTelemetry coverage Spring Boot and OpenTelemetry.

A minimal agent-based setup

A typical Spring Boot service starts with the Java agent on the JVM command line, OTLP export configured to point to a backend, and log correlation turned on so trace IDs show up in application logs. For teams that want vendor-neutral instrumentation without rebuilding business code, that gets the service into a usable state quickly.

For manual spans, the OpenTelemetry API can wrap business logic directly. That's useful around expensive sections that automatic instrumentation won't capture cleanly, such as a complicated pricing rule or a batch loop that fans out to several internal components. The rule is to instrument the points that carry business meaning, not every method in the codebase.

A production setup also benefits from explicit context handling in async code. Spring's OpenTelemetry support highlights that thread switches can lose context unless the executor is configured to propagate it, which is exactly why trace IDs disappear in @Async paths if nothing is done about them Spring Boot and OpenTelemetry.

Verify the setup before trusting it

The best validation is not “the app starts.” It's whether a real request appears with the expected span tree and whether logs can be correlated back to that trace ID. If the backend shows the inbound HTTP span but not the downstream JDBC span, the issue is usually instrumentation coverage, not export.

The agent path is especially attractive in greenfield Spring Boot services because it supports fast rollout. Manual instrumentation is better when a service has sharp performance requirements or business-critical code paths that deserve explicit spans. Many teams use both, agent first, targeted manual spans second.

A laptop on a wooden desk displaying Java code, next to a coffee mug and technical books.

The main thing to avoid is blanket instrumentation of everything that moves. That creates traces that are technically correct and operationally useless.

Later, when the service is already emitting useful traces, a backend like Jaeger can be used to inspect the waterfall and confirm span relationships. A short live demo is often more convincing than a long checklist.

Managing Overhead and Sampling in Production

Tracing overhead is why disciplined teams do not trace everything. The cost is real, even when it stays modest. A SPEC ICPE paper on accurate object tracing reported an average runtime overhead of 4.68%, and another SPEC ICPE study on bcc-java reported a geometric mean overhead of less than 5% across benchmarks, with one outlier reaching 37% because of unusually high futex system calls SPEC ICPE tracing overhead research. Treat those figures as guardrails, not permission to instrument every code path.

Sampling is the cost-control lever

Head-based sampling decides at trace start. It is cheap, predictable, and easy to run at high volume, which makes it a practical default for busy Java services. Tail-based sampling waits until the trace completes, then decides whether to keep the full trace. That approach is better for preserving error paths and unusually slow requests, but it asks more of your tracing backend and your budget.

The trade-off is straightforward. Head-based sampling reduces overhead and storage pressure, but it can skip the one request you later need. Tail-based sampling keeps more useful outliers, but it needs more infrastructure and tighter cost control.

A production setup should keep error traces longer than successful ones and sample routine traffic conservatively. That matches how incident reviews work. Most requests are routine, and routine traffic should not consume most of the budget. For a practical way to monitor the cost of that decision, see the monitor application performance guide.

Granularity is part of the budget

Span design matters just as much. If every helper method becomes a span, the trace turns into noise. If everything collapses into one span, the trace loses diagnostic value. The better pattern is to create child spans around the boundaries that matter, then attach attributes to details that do not need their own timeline entry.

Scale changes the cost curve quickly. One study recorded more than 100,000 traces for the Eclipse benchmark under baseline conditions, then rose to 270,000 traces and 33 MB of heap usage in a trace-based variant SPEC ICPE tracing overhead research. That kind of growth is why teams watch trace volume as closely as CPU and latency.

Operational rule: if a span does not help an on-call engineer decide faster, it probably belongs as an attribute or a metric.

Tracing stays viable when you monitor the tracer itself. JVM metrics, exporter queues, and backend ingestion latency show when tracing starts to become part of the problem. In a healthy system, tracing stays quiet until the incident arrives. The Arch guide to performance monitoring is a useful companion because it treats observability as layered work, not a single dashboard problem.

Connecting Traces with Logs and Metrics

Traces do not replace logs and metrics. They sit between them and give the request path enough structure to make the other two signals useful. Metrics tell operators that something changed, logs explain the local event stream, and traces connect the dots across services.

The cleanest workflow starts when logs carry the current trace ID and span ID. Spring Boot's OpenTelemetry integration supports that correlation pattern, which lets an engineer move from a slow span to the exact log lines produced by the same request Spring Boot and OpenTelemetry. That is faster than searching by timestamp alone, especially when several requests overlap.

Use each signal for the job it handles best

Metrics should trigger the investigation. Error rates, queue depth, saturation, and latency distributions tell the on-call engineer whether a service is under stress. Traces then narrow the search to the hop that absorbed the time. Logs finish the job by exposing the exception, SQL text, or warning attached to the request.

That workflow is why a trace is often the entry point, not the finish line. A useful trace tells the operator where to look next. It usually does not contain every reason the system misbehaved, and it should not try to.

The Arch guide to performance monitoring is a useful companion here because it treats observability as layered work, not a single dashboard problem. Teams that keep metrics, traces, and logs separate in storage but joined in practice usually get faster incident response than teams that force every signal into one view.

Keep the correlation path boring

Correlation works best when the application emits structured logs and the trace ID is easy to find. The log format should stay stable, the request boundary should always carry the context, and dashboards should show trace duration next to throughput and errors.

That is also where cost control starts to matter. If trace volume is high, correlation is only useful when you can afford to keep it running during real traffic, so sampling needs to be deliberate. Head-based sampling is cheap and predictable, while tail-based sampling can keep the expensive, interesting requests, but only if your backend can handle the extra work. In practice, many teams keep a low baseline sample rate, then raise it for specific routes, tenants, or incidents instead of tracing everything all the time.

An internal runbook for monitoring application performance is useful when different people need to repeat that correlation path under pressure. Good observability is not just data collection, it is repeatable navigation under stress.

Debugging a Real Production Incident with Traces

An e-commerce checkout service starts returning 5-second response times during peak traffic. The first guess is the payment gateway because that is where the customer-facing latency feels most visible, but the trace tells a different story. The payment span is present, but it isn't the one holding the request open.

Follow the request, not the assumption

The p99 alert points to checkout latency, so the engineer opens the slowest trace and checks the child spans in order. The inventory service shows a long database span, then a wait on connection acquisition, then a delay before the request returns to checkout. The trace makes the dependency chain visible, and that visibility removes the guesswork.

The span attributes point to the exact SQL path, which is enough to tell that the issue is not the payment provider. The request is stalling because inventory cannot get a database connection quickly enough, and the query shape suggests a missing index is making the connection pool starvation worse. That is the kind of distinction tracing is built to reveal.

The log correlation confirms it. The trace ID lines up with connection pool warnings at the same time the slow request was active, which shows the incident was local to the inventory service, not a downstream network failure. That combination of trace, log, and metric evidence is what closes the loop.

For teams working in regulated or high-stakes environments, the same pattern applies in different domains. A healthcare observability implementation often depends on the same discipline, trace the request path first, then use metrics and logs to prove the cause before changing anything.

What the on-call engineer actually does

The response sequence is straightforward:

  • Open the slowest trace: confirm which span consumed the time.
  • Inspect span attributes: identify the service, operation, and database call.
  • Check the correlated logs: look for pool exhaustion, retries, or SQL warnings.
  • Compare against service metrics: verify whether the pool, CPU, or thread count was saturated.
  • Fix the bottleneck: in this case, review the query plan and the index strategy.

That incident pattern is common because tracing rarely gives the final root cause by itself. It gives the shortest path to the right team and the right subsystem. That's still a huge win during an outage.

Incident response best practices pair well with that workflow because they keep the investigation disciplined when the temptation is to jump straight to a fix. In practice, the highest value traces are the ones that prevent the wrong fix from being shipped.


Fivenines helps teams keep the operational side of observability simple, with fast monitoring, alerting, and clear incident visibility that complements tracing instead of replacing it. If tracing in Java is already part of the workflow, Fivenines gives teams a practical way to keep the rest of the stack under control and respond faster when production turns noisy.