Java applications generate observability problems that other languages rarely encounter at the same scale. A Spring Boot service running on the JVM can look perfectly healthy on CPU and memory dashboards while its garbage collector pauses all threads every few seconds, adding invisible latency to every request that lands during a GC cycle. A Hibernate ORM layer can execute 47 database queries for a single API call because of an N+1 relationship that the developer never noticed during testing. A connection pool can silently queue all incoming requests once it exhausts its configured limit, producing rising p99 latency with no error in any log file.
Full Java observability requires three signal types working together: structured logs correlated to traces via MDC (Mapped Diagnostic Context), JVM and application metrics covering heap, GC, connection pools, and request rates, and distributed traces showing the exact call path that caused a specific slow request. The OpenTelemetry Java agent provides zero-code instrumentation for all three signals. The current stable release is opentelemetry-java-instrumentation v2.28.1 (May 2026), targeting the OpenTelemetry Java SDK v1.63.0 (June 2026).
This guide covers structured logging with Logback, zero-code instrumentation with the OTel Java agent, manual spans for business logic, custom metrics, Spring Boot integration options, and sending all three signals to CubeAPM.
Key Takeaways
- The OpenTelemetry Java agent (v2.28.1, May 2026) provides zero-code instrumentation for Spring, Hibernate, JDBC, Kafka, gRPC, Redis, and Logback/Log4j2 MDC injection via a single -javaagent JAR flag.
- JAVA_TOOL_OPTIONS is the cleanest way to attach the agent in container images and application servers; it is picked up automatically by any JVM process.
- The OTel Java agent automatically injects traceId, spanId, and traceFlags into MDC for both Logback and Log4j2 with no code changes required.
- CVE-2026-33701 affects RMI instrumentation in versions before v2.28.1; upgrade to v2.26.1 or later (current latest is v2.28.1).
- opentelemetry-exporter-zipkin is deprecated in v1.63.0 (June 2026) with a last planned release in August 2026; migrate to OTLP.
- Spring Boot 4.0 (November 2025) ships a first-party spring-boot-starter-opentelemetry; a log export bug in 4.0.0 is fixed in 4.0.1+.
- The community OTel Spring Boot starter (io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter) supports Spring Boot 2.6+ and 3.1+ and works with GraalVM Native Image where the agent does not.
- logstash-logback-encoder 9.0 (October 2025) requires Logback 1.3.0+ and Jackson 3.0.0+.
Why Java Observability Has Specific Challenges
- GC pauses are invisible to application-level monitoring: The JVM can stop all threads for a garbage collection pause of 50ms to several seconds. From the application’s perspective, time simply stops. Requests that arrive during the pause queue up and then all complete at once after the pause ends. The result is a latency spike with no error in the logs and no slow span in any individual trace. Monitoring GC pause time and GC frequency as metrics is the only way to detect this pattern.
- N+1 queries are not visible without SQL-level tracing: Hibernate and Spring Data JPA generate SQL queries dynamically. A @OneToMany relationship without FetchType.EAGER or a join fetch causes Hibernate to issue one query per child entity rather than one query for all of them. A list endpoint returning 50 orders, each with 10 line items, executes 501 queries instead of 1. Standard logging shows the endpoint was slow. Only SQL-level trace spans show the 501 queries.
- Thread pool exhaustion produces queuing, not errors: When all threads in a Tomcat, Jetty, or Undertow thread pool are busy, new requests queue at the connector level. The application logs nothing because no request has been rejected, just delayed. p99 latency climbs while p50 stays flat. Monitoring active threads, queued requests, and pool utilization as metrics is the only way to detect this before it becomes a timeout.
Step 1: Structured Logging with Logback and MDC Trace Injection
Logback is the default logging framework in Spring Boot and the most widely used in the Java ecosystem. Configure it to output structured JSON using the logstash-logback-encoder library (current stable release: 9.0, October 2025), which requires Logback 1.3.0+ and Jackson 3.0.0+.
Add the dependency to pom.xml:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>9.0</version>
</dependency>Configure logback-spring.xml (for Spring Boot) to output JSON:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="JSON_STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<fieldNames>
<timestamp>timestamp</timestamp>
<version>[ignore]</version>
<levelValue>[ignore]</levelValue>
</fieldNames>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_STDOUT"/>
</root>
</configuration>Use SLF4J with MDC to add contextual fields to log records:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order processOrder(String orderId, String userId) {
MDC.put("orderId", orderId);
MDC.put("userId", userId);
try {
log.info("Processing order");
// ... business logic
log.info("Order processed successfully");
return order;
} finally {
MDC.clear();
}
}
}This produces JSON output with every field searchable:
{
"@timestamp": "2026-06-09T10:22:31.412Z",
"level": "INFO",
"logger_name": "com.example.OrderService",
"message": "Processing order",
"orderId": "ord_123",
"userId": "usr_456"
}Injecting trace context into logs via MDC: When the OpenTelemetry Java agent is attached (Step 2 below), it automatically injects the active traceId, spanId, and traceFlags into MDC. These fields appear in every log record produced during that request, enabling direct correlation from a log event to the trace that produced it. No code changes are required. The agent handles it via the Logger MDC auto-instrumentation, which is enabled by default for both Logback and Log4j2.
Step 2: Zero-Code Instrumentation with the OTel Java Agent
The OpenTelemetry Java agent is a single JAR that attaches to the JVM at startup using the -javaagent flag. It instruments all supported libraries via bytecode manipulation at class load time, with no code changes required.
Download the latest agent JAR:
curl -L -O https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jarThe current stable release is v2.28.1 (May 2026).
Attach the agent at JVM startup:
java -javaagent:./opentelemetry-javaagent.jar \
-Dotel.service.name=my-java-service \
-Dotel.exporter.otlp.endpoint=http://your-cubeapm-instance:4317 \
-Dotel.exporter.otlp.protocol=grpc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar myapp.jarOr configure entirely via environment variables, which is the cleaner approach for containerized deployments:
export JAVA_TOOL_OPTIONS="-javaagent:/opt/otel/opentelemetry-javaagent.jar"
export OTEL_SERVICE_NAME=my-java-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://your-cubeapm-instance:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlpJAVA_TOOL_OPTIONS is picked up by any JVM process automatically, making it the cleanest approach for container images and application servers.
What the agent instruments automatically:
| Library / Framework | What is instrumented |
| Spring MVC / Spring Boot | HTTP request spans with route pattern, method, status code |
| Spring WebFlux | Reactive HTTP request spans |
| Jakarta EE / JAX-RS | HTTP request spans for JAX-RS resources |
| Servlet API | Inbound HTTP request spans for any servlet container |
| JDBC | SQL query spans with sanitized query text, DB type, and duration |
| Hibernate ORM | ORM operation spans on top of JDBC |
| Kafka (producer/consumer) | Producer send spans, consumer process spans with topic and partition |
| gRPC | Client and server call spans |
| Spring Data | Repository method spans |
| Redis (Jedis, Lettuce) | Redis command spans |
| Apache HttpClient / OkHttp | Outbound HTTP client call spans |
| Logback / Log4j2 | MDC injection of traceId and spanId (no code changes needed) |
| Spring Scheduling / Quartz | Scheduled task execution spans |
Security note: CVE-2026-33701 was disclosed in May 2026, affecting the RMI instrumentation in opentelemetry-java-instrumentation prior to v2.26.1. The vulnerability allows unsafe deserialization that could lead to remote code execution. It is fixed in v2.26.1 and later. Ensure you are using v2.26.1 or above. The current latest is v2.28.1.
Note on Zipkin exporter: opentelemetry-exporter-zipkin is deprecated as of opentelemetry-java v1.63.0 (June 2026), with its last planned release in August 2026. Migrate to OTLP before the final release.
Note on Java 25: Java 25 was released as LTS in September 2025. The OTel Java agent has experimental Java 25 support, but agent-transformed bytecode verification changes introduced in Java 25 can cause compatibility issues with some instrumentations. For production deployments, Java 21 LTS is the stable recommended target until full Java 25 support is confirmed.
Step 3: Manual Instrumentation for Custom Spans
The Java agent covers framework and library calls. Add manual spans for business-critical operations that sit between framework boundaries.
Add the OTel API dependency to pom.xml. The API is stable and safe to use as a direct application dependency:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.63.0</version>
</dependency>Get a tracer and create a child span:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class PricingService {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("com.example.PricingService", "1.0.0");
public PricingResult calculatePricing(String orderId, List<LineItem> items) {
Span span = tracer.spanBuilder("calculate_pricing")
.setAttribute("order.id", orderId)
.setAttribute("order.item_count", items.size())
.startSpan();
try (Scope scope = span.makeCurrent()) {
PricingResult result = pricingEngine.compute(items);
span.setAttribute("pricing.total", result.getTotal());
span.setAttribute("pricing.discount_applied", result.isDiscountApplied());
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}For Spring applications, the @WithSpan annotation from opentelemetry-instrumentation-annotations creates a span automatically around a method without try-with-resources boilerplate. Note: @WithSpan requires spring-boot-starter-aop when used with the Spring Boot OTel starter:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>2.28.1</version>
</dependency>import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
@Service
public class RecommendationService {
@WithSpan("generate_recommendations")
public List<Product> getRecommendations(
@SpanAttribute("user.id") String userId,
@SpanAttribute("category") String category) {
// Method body automatically wrapped in a span
return recommendationEngine.compute(userId, category);
}
}Step 4: Spring Boot Integration Options
There are three ways to integrate OpenTelemetry with Spring Boot, depending on your Spring Boot version and requirements.
Option A: OTel Java Agent (recommended default for all Spring Boot versions)
Attach the agent as described in Step 2. This is the OTel project’s own recommended default for Spring Boot applications and provides the broadest auto-instrumentation coverage.
Option B: OpenTelemetry Spring Boot Starter (Spring Boot 2.6+ and 3.1+)
The community OTel starter from io.opentelemetry.instrumentation works with Spring Boot 2.6+ and Spring Boot 3.1+, and supports GraalVM Native Image (where the Java agent does not work). Use this when the agent’s startup overhead is a concern, or when deploying as a native image.
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>2.28.1</version>
</dependency>Configure in application.properties:
otel.exporter.otlp.endpoint=http://your-cubeapm-instance:4318
otel.traces.exporter=otlp
otel.metrics.exporter=otlp
otel.logs.exporter=otlp
otel.service.name=my-spring-service
management.tracing.sampling.probability=1.0Note: the starter is marked as stable but pulls in some alpha-suffixed dependencies.
Option C: Spring Boot 4 native OTel starter (Spring Boot 4.0+ only)
Spring Boot 4.0 (released November 20, 2025, current stable: 4.0.5) ships a first-party starter spring-boot-starter-opentelemetry that uses Micrometer internally and exports traces, metrics, and logs via OTLP. Configuration follows the management.* prefix:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>Configure in application.properties:
management.opentelemetry.resource-attributes.service.name=my-spring-service
management.opentelemetry.tracing.export.otlp.endpoint=http://your-cubeapm-instance:4318
management.tracing.sampling.probability=1.0Note on Spring Boot 4.0.0 and logs: A bug in Spring Boot 4.0.0 prevents log export unless spring-boot-starter-actuator is also on the classpath. Upgrade to Spring Boot 4.0.1 or higher to avoid this, or add spring-boot-starter-actuator as a workaround.
Key JVM metrics exposed automatically via Micrometer (all three options):
| Metric | Description |
| jvm.memory.used | Heap and non-heap memory used in bytes |
| jvm.gc.pause | GC pause duration histogram |
| jvm.gc.memory.promoted | Bytes promoted from young to old generation per GC |
| jvm.threads.live | Current number of live threads |
| jvm.classes.loaded | Number of classes currently loaded |
| http.server.requests | HTTP request rate, duration, and status code histogram |
| hikaricp.connections.active | Active HikariCP connection pool connections |
| hikaricp.connections.pending | Requests waiting for a connection from the pool |
Step 5: Custom Metrics with the OTel Metrics API
For business-level metrics not provided by auto-instrumentation, use the OTel Metrics API:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.common.Attributes;
public class OrderMetrics {
private static final Meter meter =
GlobalOpenTelemetry.getMeter("com.example.orders", "1.0.0");
private static final LongCounter ordersProcessed = meter
.counterBuilder("orders.processed")
.setDescription("Total number of orders processed")
.setUnit("1")
.build();
private static final LongHistogram orderProcessingDuration = meter
.histogramBuilder("orders.processing.duration")
.ofLongs()
.setDescription("Time to process an order in milliseconds")
.setUnit("ms")
.build();
public static void recordOrderProcessed(String orderType, String region) {
ordersProcessed.add(1,
Attributes.builder()
.put("order.type", orderType)
.put("region", region)
.build());
}
public static void recordProcessingDuration(long durationMs, String orderType) {
orderProcessingDuration.record(durationMs,
Attributes.builder()
.put("order.type", orderType)
.build());
}
}Step 6: Monitor Java Application Health with CubeAPM

CubeAPM receives all three signal types from Java applications via OTLP: traces, metrics, and logs. Setting OTEL_EXPORTER_OTLP_ENDPOINT to your CubeAPM instance and attaching the Java agent is all the configuration required.
What CubeAPM monitors for Java applications:
- HTTP request rate, error rate, and p99 latency per endpoint (Spring MVC, JAX-RS, Servlet)
- SQL query spans with sanitized query text, duration, and error status (JDBC, Hibernate)
- JVM heap usage, GC pause time, and GC frequency
- Thread pool utilization (active threads, queued requests) from HikariCP and Tomcat metrics
- Outbound HTTP client spans (Apache HttpClient, OkHttp)
- Kafka producer and consumer spans with topic, partition, and consumer group
- Custom business metrics and spans
- Structured logs (Logback JSON) correlated to traces via MDC-injected traceId and spanId
- Distributed traces across Java microservices with end-to-end flame graphs
Key alerts to configure for Java applications in CubeAPM:
| Alert | Condition | Severity |
| High error rate | HTTP error rate > 1% for 5 min | Warning |
| High p99 latency | p99 request duration > 2,000 ms | Warning |
| GC pressure | GC pause time rate > 200ms/s | Warning |
| High heap usage | jvm.memory.used > 80% of max heap | Warning |
| Connection pool exhaustion | hikaricp.connections.pending > 0 for 5 min | Warning |
| Slow SQL queries | Any JDBC span > 500 ms | Warning |
| N+1 query pattern | JDBC span count per trace > 20 | Warning |
| Thread pool saturation | Active threads approaching pool maximum | Warning |
Read the docs to configure OTLP ingestion and Java application monitoring.
Summary
Java observability requires monitoring JVM internals alongside application behavior. GC pauses, connection pool exhaustion, and N+1 query patterns are all invisible to standard server monitoring. The OpenTelemetry Java agent provides zero-code instrumentation for all of these in a single JAR attached at startup.
| Signal | Collection method | Key data |
| Structured logs | Logback + logstash-logback-encoder 9.0 + MDC traceId/spanId injection | JSON log lines with trace context, auto-injected by OTel agent |
| Distributed traces | OTel Java agent v2.28.1 + @WithSpan for custom spans | HTTP, JDBC, Hibernate, Kafka, gRPC, Redis spans |
| JVM metrics | Micrometer (all Spring Boot options) or OTel agent JVM metrics | Heap, GC pause, threads, class loading, HikariCP pool |
| Custom metrics | OTel Metrics API (counterBuilder, histogramBuilder) | Business counters, duration histograms |
| Zero-code setup | -javaagent:opentelemetry-javaagent.jar via JAVA_TOOL_OPTIONS | All frameworks and libraries, no code changes |
Disclaimer: All OTel Java package versions and API calls sourced from official OTel Java documentation at opentelemetry.io/docs/languages/java/ and GitHub releases, verified June 2026. Current stable: opentelemetry-java SDK v1.63.0 (June 6, 2026); opentelemetry-java-instrumentation v2.28.1 (May 21, 2026). CVE-2026-33701 affects RMI instrumentation before v2.26.1; upgrade to v2.26.1 or later. opentelemetry-exporter-zipkin deprecated in v1.63.0, last planned release August 2026 (source: github.com/open-telemetry/opentelemetry-java/releases). Default OTLP endpoint for the Java agent is http://localhost:4317 (gRPC). logstash-logback-encoder 9.0 (October 2025) requires Logback 1.3.0+ and Jackson 3.0.0+ (source: github.com/logfellow/logstash-logback-encoder). Community OTel Spring Boot starter (io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter) supports Spring Boot 2.6+ and 3.1+ (source: opentelemetry.io/docs/zero-code/java/spring-boot-starter/). Spring Boot 4.0 native starter (spring-boot-starter-opentelemetry) introduced in Spring Boot 4.0 (November 20, 2025); log export bug present in 4.0.0, fixed in 4.0.1+. Java 25 (LTS, September 2025): OTel Java agent support is experimental; Java 21 LTS recommended for production. CubeAPM: $0.15/GB, no per-service or per-host fees.
Also read:
Observability for Node.js Applications: Logs, Metrics, and Traces
Observability for Python Applications: Logs, Metrics, and Traces
Observability for Serverless Applications on AWS Lambda: What to Track and How





