OpenTelemetry Logs for Java

This document covers OpenTelemetry Logs for Java, focusing on integration with popular logging frameworks.

Prerequisites

Make sure the OpenTelemetry Java Agent is configured before you start instrumenting code. Follow Getting started with OpenTelemetry Java first.

If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to OpenTelemetry Logs first.

Overview

OpenTelemetry provides two approaches for collecting logs in Java:

  1. Java Agent (recommended): The OpenTelemetry Java Agent automatically captures logs from popular logging frameworks (Log4j2, Logback, java.util.logging) and correlates them with traces.
  2. Manual instrumentation: Use OpenTelemetry logging libraries directly for fine-grained control.

The Java Agent approach is recommended because it requires no code changes and automatically adds trace context (trace_id, span_id) to your logs.

Automatic Log Collection with Java Agent

The OpenTelemetry Java Agent automatically captures logs from:

  • Log4j2 (2.x)
  • Logback (1.x)
  • java.util.logging (JUL)

Configuration

Enable log export by setting the OTEL_LOGS_EXPORTER environment variable:

shell
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev:4317
export OTEL_EXPORTER_OTLP_HEADERS="uptrace-dsn=<FIXME>"

Then run your application with the Java Agent:

shell
java -javaagent:path/to/opentelemetry-javaagent.jar \
     -jar myapp.jar

All logs emitted by your application will be automatically captured and exported to Uptrace with trace context.

Log4j2 Integration

Log4j2 is a popular logging framework for Java. The OpenTelemetry Java Agent automatically captures Log4j2 logs.

Basic Usage

java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class MyService {
    private static final Logger logger = LogManager.getLogger(MyService.class);

    public void processRequest(String userId) {
        logger.info("Processing request for user: {}", userId);

        try {
            // Business logic
            performOperation();
            logger.debug("Operation completed successfully");
        } catch (Exception e) {
            logger.error("Failed to process request", e);
        }
    }

    private void performOperation() {
        // Your business logic
    }
}

Manual Instrumentation with OpenTelemetry Appender

For more control, you can use the OpenTelemetry Log4j2 Appender directly:

Add the dependency:

xml Maven
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-log4j-appender-2.17</artifactId>
    <version>2.11.0-alpha</version>
    <scope>runtime</scope>
</dependency>
gradle Gradle
runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-log4j-appender-2.17:2.11.0-alpha")

Configure the appender in log4j2.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
        <OpenTelemetry name="OpenTelemetry"/>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="OpenTelemetry"/>
        </Root>
    </Loggers>
</Configuration>

See OpenTelemetry Log4j2 for details.

Logback Integration

Logback is another popular logging framework, often used with Spring Boot. The OpenTelemetry Java Agent automatically captures Logback logs.

Basic Usage

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
    private static final Logger logger = LoggerFactory.getLogger(MyService.class);

    public void processRequest(String userId) {
        logger.info("Processing request for user: {}", userId);

        try {
            // Business logic
            performOperation();
            logger.debug("Operation completed successfully");
        } catch (Exception e) {
            logger.error("Failed to process request", e);
        }
    }

    private void performOperation() {
        // Your business logic
    }
}

Manual Instrumentation with OpenTelemetry Appender

For more control, you can use the OpenTelemetry Logback Appender directly:

Add the dependency:

xml Maven
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-logback-appender-1.0</artifactId>
    <version>2.11.0-alpha</version>
    <scope>runtime</scope>
</dependency>
gradle Gradle
runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:2.11.0-alpha")

Configure the appender in logback.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="OpenTelemetry"
              class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
    </appender>

    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="OpenTelemetry"/>
    </root>
</configuration>

See OpenTelemetry Logback for details.

Log-Trace Correlation

When you emit a log within an active trace span, OpenTelemetry automatically includes:

  • trace_id: Links log to the entire distributed trace
  • span_id: Links log to the specific operation
  • trace_flags: Indicates if the trace is sampled

This enables bidirectional navigation between logs and traces in Uptrace.

Example with Tracing

java
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    private static final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");

    public void processOrder(String orderId) {
        Span span = tracer.spanBuilder("process-order").startSpan();
        try (Scope scope = span.makeCurrent()) {
            // Log automatically includes trace_id and span_id
            logger.info("Starting order processing for orderId={}", orderId);

            validateOrder(orderId);
            chargePayment(orderId);
            sendConfirmation(orderId);

            logger.info("Order processing completed for orderId={}", orderId);
        } catch (Exception e) {
            logger.error("Order processing failed for orderId={}", orderId, e);
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }

    private void validateOrder(String orderId) {
        logger.debug("Validating order orderId={}", orderId);
        // Validation logic
    }

    private void chargePayment(String orderId) {
        logger.debug("Charging payment for orderId={}", orderId);
        // Payment logic
    }

    private void sendConfirmation(String orderId) {
        logger.debug("Sending confirmation for orderId={}", orderId);
        // Confirmation logic
    }
}

Manual Correlation

If you can't use automatic instrumentation, manually inject trace context using MDC (Mapped Diagnostic Context):

java
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import org.slf4j.MDC;

public class TraceContextHelper {

    public static void addTraceContext() {
        SpanContext spanContext = Span.current().getSpanContext();
        if (spanContext.isValid()) {
            MDC.put("trace_id", spanContext.getTraceId());
            MDC.put("span_id", spanContext.getSpanId());
            MDC.put("trace_flags", spanContext.getTraceFlags().asHex());
        }
    }

    public static void clearTraceContext() {
        MDC.remove("trace_id");
        MDC.remove("span_id");
        MDC.remove("trace_flags");
    }
}

Then update your logging pattern to include MDC values:

xml
<!-- Logback pattern -->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} - %msg%n</pattern>

Best Practices

Use Structured Logging

Use key-value pairs for structured logging to enable filtering:

java
// Good: Structured logging with key-value pairs
logger.info("Database query executed query_type={} table={} duration_ms={} rows_affected={}",
    "SELECT", "users", 45, 1);

// Bad: Unstructured message
logger.info("Executed SELECT query on users table, took 45ms, returned 1 row");

Use Appropriate Log Levels

java
// TRACE: Very detailed information, typically only useful during development
logger.trace("Entering method processRequest with param={}", param);

// DEBUG: Detailed information useful for debugging
logger.debug("Cache miss for key={}", cacheKey);

// INFO: General information about application flow
logger.info("User logged in userId={}", userId);

// WARN: Potentially harmful situations
logger.warn("Retry attempt count={} for operation={}", retryCount, operationName);

// ERROR: Error events that might still allow the application to continue
logger.error("Failed to process request", exception);

Avoid Logging Sensitive Data

Never log passwords, tokens, or PII:

java
// Bad: Logging sensitive data
logger.info("User login password={}", password);

// Good: Redact sensitive fields
logger.info("User login userId={}", userId);

Avoid String Concatenation

Use parameterized logging instead of string concatenation:

java
// Bad: String concatenation (always evaluates arguments)
logger.debug("Processing order " + orderId + " for user " + userId);

// Good: Parameterized logging (lazy evaluation)
logger.debug("Processing order orderId={} userId={}", orderId, userId);

What's next?