Observability for Java with OpenTelemetry

Khimananda Oli 7 min read Programming and Languages
Observability for Java with OpenTelemetry

By Khimananda Oli | Last reviewed: August 2026

Debugging latency spikes or silent failures in distributed JVM applications is nearly impossible without unified telemetry. Observability for Java with OpenTelemetry solves this by standardizing how you collect traces, metrics, and logs across Spring Boot, Quarkus, and legacy stacks. Instead of vendor-locked agents, you gain portable, high-fidelity data that works with any backend.

How does observability for Java with OpenTelemetry work?

At its core, OpenTelemetry decouples data generation from data analysis. For Java, this happens through two primary mechanisms: bytecode manipulation and API injection. The Java Agent uses the Java Instrumentation API to modify classes at runtime, injecting context propagation and span creation into supported libraries like Hibernate, Kafka, and Tomcat without touching your source code. This is distinct from older APM tools because the data format is open and vendor-neutral.

Java ApplicationBusiness LogicOTel Java AgentBytecode InstrumentationOTel CollectorBatch / Filter / RouteTracing BackendMetrics StoreLog AggregatorOTLP/gRPC
Observability for Java with OpenTelemetry architecture: Agent captures signals and forwards via OTLP to collector and backends

The agent acts as a bridge between your application internals and the outside world. It automatically propagates W3C Trace Context headers across HTTP, gRPC, and messaging boundaries. When a request enters your Spring Boot controller, the agent creates a server span. When that controller calls a PostgreSQL database, it creates a child client span. These are linked by a shared trace ID, enabling end-to-end visibility even in complex microservices architectures common in Nepal's growing fintech sector where transaction integrity is paramount.

How do you configure the OpenTelemetry Java Agent?

The most reliable path to instrumenting an app with OpenTelemetry is the zero-code Java Agent. Download the latest opentelemetry-javaagent.jar from the official GitHub releases. Attach it to your JVM startup command. This method survives framework upgrades and requires no build-time dependencies.

java -javaagent:./opentelemetry-javaagent.jar \
     -Dotel.service.name=payment-service \
     -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
     -Dotel.metrics.exporter=prometheus \
     -jar payment-service-1.0.0.jar

Essential configuration properties

  • otel.service.name: Mandatory logical name. Use consistent naming conventions (e.g., env-team-service) to avoid dashboard fragmentation.
  • otel.exporter.otlp.endpoint: Your OTel Collector or backend ingestion URL. Prefer gRPC (port 4317) over HTTP/protobuf for lower overhead in high-throughput Java apps.
  • otel.instrumentation.[name].enabled: Disable noisy or problematic instrumentations. For example, -Dotel.instrumentation.jdbc.enabled=false if you use a custom connection pool that conflicts.
  • otel.traces.sampler: Control volume. Use parentbased_tracealways for debugging or traceidratio with a 0.1 value for high-traffic production systems to manage cost.

In containerized environments like Kubernetes, pass these as environment variables rather than JVM flags. This keeps your Dockerfile generic. Set OTEL_SERVICE_NAME via the Downward API to automatically tag pods with their deployment name, ensuring metadata accuracy without manual intervention.

When should you use manual instrumentation in Java?

Auto-instrumentation covers infrastructure and framework boundaries, but it cannot understand business semantics. You need the OpenTelemetry SDK when tracking domain-specific operations like "calculate-tax" or "validate-kyc-document". Manual spans provide the context that turns raw traces into actionable intelligence during incident response.

HTTP RequestAuto Span (Controller)Auto Span (Service)Manual Span (Business)validateOrder()applyDiscount()persistLedger()SDK.createSpan()
Manual instrumentation nesting: Business logic spans enrich auto-generated framework traces for deeper observability

Add the opentelemetry-api dependency to your project. Never add the full SDK as a compile dependency; let the agent inject it at runtime to prevent version conflicts. Use the global tracer provider to create spans around critical paths.

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

Tracer tracer = GlobalOpenTelemetry.getTracer("payment-domain");

public void processRefund(String orderId) {
    Span span = tracer.spanBuilder("process-refund")
        .setAttribute("order.id", orderId)
        .setAttribute("refund.type", "FULL")
        .startSpan();
    
    try (var scope = span.makeCurrent()) {
        // Business logic here
        refundGateway.execute(orderId);
    } catch (Exception e) {
        span.recordException(e);
        span.setStatus(StatusCode.ERROR);
        throw e;
    } finally {
        span.end();
    }
}

Always use try-with-resources or explicit makeCurrent() scopes. Failing to close scopes causes context leaks that corrupt downstream traces and create memory pressure in long-running JVM processes. In my experience auditing financial systems, missing span.end() calls are the #1 cause of incomplete trace data.

How do you correlate logs and metrics with Java traces?

Traces alone don't tell the full story. True correlation between metrics, logs, and traces requires injecting trace context into your logging MDC and metric attributes. The OTel Java Agent handles this automatically for SLF4J/Logback and Micrometer, but you must verify the configuration.

Signal TypeCorrelation MechanismKey ConfigurationCommon Pitfall
LogsMDC Injection (trace_id, span_id)-Dotel.logs.exporter=otlpMissing MDC pattern in logback.xml
MetricsExemplars + Resource Attributes-Dotel.metrics.exemplar.filter=trace_basedPrometheus scraping ignoring exemplars
TracesW3C Context PropagationDefault enabledCustom thread pools breaking context

For Logback, update your pattern layout to include %X{trace_id}-%X{span_id}. This allows you to jump directly from a log line in Grafana Loki to the exact span in Tempo or Jaeger. For metrics, enable exemplars to attach trace IDs to histogram buckets. This is invaluable when investigating p99 latency spikes—you can see which specific requests contributed to the tail latency rather than guessing.

What are common performance pitfalls with OTel in Java?

Observability has a cost. Without guardrails, monitoring golden signals can itself degrade the signals you're trying to measure. The Java Agent adds overhead to every instrumented call. In high-throughput systems processing thousands of TPS, unbounded sampling and synchronous exports will increase GC pressure and CPU usage.

  1. Unbounded Cardinality: Never put user IDs, session tokens, or IP addresses as span attributes or metric labels. This explodes backend storage costs and slows queries. Use allowlists for attribute keys.
  2. Synchronous Exporters: Always use async exporters with batching. The default OTLP exporter batches by time and size, but misconfigured timeouts can block application threads during network partitions.
  3. Context Propagation Failures: Custom executors and reactive streams often break ThreadLocal context. Wrap your runnables with Context.current().wrap(runnable) or use the OTel-provided concurrent utilities.
  4. Over-Instrumentation: Disable instrumentations for internal health checks, readiness probes, and admin endpoints. These generate noise and consume quota without adding debugging value.
Unoptimized OTelCPU Overhead: +15-25%Memory: High GC PressureSampling: 100% All RequestsExport: Synchronous BlocksOptimized OTelCPU Overhead: +2-5%Memory: Stable Heap UsageSampling: Ratio + Tail-BasedExport: Async Batched OTLPTuning
Performance impact comparison: Optimized observability for Java with OpenTelemetry reduces overhead from 25% to under 5%

Implement tail-based sampling in your OTel Collector to keep 100% of error traces while sampling only 1% of successful ones. This preserves debugging fidelity for failures while controlling costs for happy paths. For Java specifically, monitor the otel.javaagent thread group in your profiler. If you see excessive allocation in io.opentelemetry.sdk.trace.export.BatchSpanProcessor, increase the batch size or reduce the export interval.

Next steps for production-ready Java observability

Start with the Java Agent in staging. Validate that trace context propagates correctly across all service boundaries before enabling in production. Define meaningful SLIs and SLOs based on the telemetry you collect—don't just dashboards for dashboards' sake. Treat your observability configuration as code: version your agent configs, sampler rules, and collector pipelines alongside your application.

If you're struggling with high-cardinality explosions, context loss in async flows, or tuning OTel for compliance-heavy environments, reach out for a consultation. I help teams build observable Java systems that survive audits and scale without breaking the bank.

Frequently Asked Questions

OpenTelemetry Java agent requires Java 8 or higher. Java 17+ is recommended for virtual threads support and latest instrumentation features in current stable releases.

Add the javaagent flag pointing to the opentelemetry-javaagent.jar file in your JVM startup arguments. Configure exporter endpoints via environment variables like OTEL_EXPORTER_OTLP_ENDPOINT before starting the application process.

Yes, the Java agent automatically instruments Spring Web MVC, WebFlux, and data access layers without code changes. It captures HTTP requests, database queries, and messaging events using bytecode manipulation at runtime.

The agent supports Tomcat, Jetty, WildFly, and WebLogic. Verify compatibility against the official supported libraries list as some older proprietary application servers may require manual instrumentation hooks or specific agent versions.

Typical overhead ranges from three to five percent for CPU and memory under normal load. Overhead increases with high cardinality attributes or excessive span creation, so always benchmark in staging environments first.

Auto instrumentation uses bytecode modification to capture telemetry without source changes. Manual instrumentation requires adding SDK dependencies and writing code to create spans, offering precise control over context propagation and custom business logic tracking.

Use the AttributeKey sampler or custom SpanProcessor implementations to redact fields before export. Configure OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS to exclude authorization tokens and PII from collected trace attributes.

Use the OTLP gRPC exporter for high-throughput production systems due to binary efficiency and streaming support. Fall back to HTTP/protobuf only when network policies block gRPC ports or when integrating with serverless platforms.

Missing context propagation usually indicates unsupported libraries or async frameworks breaking thread-local storage. Enable debug logging for the agent to identify dropped contexts and verify that W3C TraceContext headers pass through proxies correctly.

Inject trace_id and span_id into MDC using the logback or log4j appender provided by the SDK. Ensure your log aggregation backend parses these structured fields to enable direct navigation between log entries and distributed traces.

Native image support requires build-time configuration since bytecode agents cannot modify compiled binaries. Use the OpenTelemetry GraalVM extension and register all instrumented classes for reflection during the native compilation phase.

Configure parent-based trace sampling via OTEL_TRACES_SAMPLER_ARG to drop low-value traces while preserving error paths. Implement custom samplers for dynamic rate limiting based on endpoint latency thresholds or tenant-specific quotas.

Default metrics include JVM memory pools, garbage collection pauses, thread counts, and HTTP request duration histograms. Enable additional runtime metrics through system properties to monitor connection pools, cache hit ratios, and custom business counters.

Yes, use the micrometer-tracing-bridge-opentelemetry adapter to route existing metrics and traces through OTLP exporters. This preserves dashboard continuity while unifying observability signals under a single vendor-neutral standard.

Run the OTel Collector with a debug exporter or use Jaeger all-in-one Docker image to inspect emitted spans. Verify service name, resource attributes, and trace completeness before deploying configuration changes to production clusters.