CubeAPM
CubeAPM CubeAPM

Spring Boot Logging: Structured Logs with Logback and OpenTelemetry

Spring Boot Logging: Structured Logs with Logback and OpenTelemetry

Table of Contents

Plain text logs work fine when you have one service and ten requests per second. At fifty microservices and a few thousand requests per minute, unstructured logs become actively harmful — grep is useless, correlation is manual, and root cause analysis turns into a guessing game. Spring Boot’s default Logback configuration outputs human-readable text, but human-readable text cannot be queried, filtered by trace ID, or aggregated across services without expensive parsing layers downstream.

This guide covers how to configure structured JSON logging in Spring Boot using Logback, inject OpenTelemetry trace context automatically, and route logs to a collector — with working configuration, real code, and the backend considerations most guides skip entirely.

According to the CNCF Observability Survey 2024, 87% of organizations use logs as their primary signal for debugging production issues. Getting log structure right before you scale is substantially cheaper than retrofitting it after.

What Is Structured Logging in Spring Boot

Structured logging means emitting log records as machine-parseable data — typically JSON — rather than free-form text strings. Instead of:

2025-11-18 10:23:41 INFO  OrderService - Order 98123 processed in 234ms

A structured log emits:

{
  "timestamp": "2025-11-18T10:23:41.123Z",
  "level": "INFO",
  "logger": "com.example.OrderService",
  "message": "Order processed",
  "order_id": "98123",
  "duration_ms": 234,
  "trace_id": "fe9ec863c187fe62f2e674412a601165",
  "span_id": "f857539a74f125d6",
  "service.name": "order-service"
}

Every field is a named key-value pair. Log aggregation platforms like OpenSearch, Grafana Loki, or CubeAPM can index these fields directly and let you filter by order_id, group by level, or join on trace_id without a single regex.

Why it matters in microservice architectures

When a request touches an API gateway, an auth service, a product catalog, and a payment processor before returning a 500, the only way to follow that request across four services is a shared trace_id. Without structured logging, that ID lives inside a free-text string buried in four separate log files. With structured logging and OpenTelemetry context injection, the same ID is an indexed field in every log record — and a single query surfaces the complete trace.

Spring Boot 3.4 introduced native structured logging support, meaning JSON output can be enabled without adding the Logstash encoder dependency for basic cases. But native support lacks trace correlation out of the box, which is where the OpenTelemetry integration becomes critical.

Understanding what log monitoring actually does in production helps set the right expectations before investing in the plumbing described below.

How Structured Logging Works in Spring Boot with Logback

Spring Boot defaults to Logback as its logging framework. Logback handles log record creation, formatting, and routing via appenders. The JSON transformation happens at the encoder layer — a component that serializes log events into bytes before the appender writes them to console, file, or network.

The two instrumentation workflows

OpenTelemetry provides two distinct workflows for getting logs from a Spring Boot application into a backend. Understanding the difference matters before touching any configuration.

Workflow 1 — Direct to collector via OTLP (OpenTelemetryAppender)

The opentelemetry-logback-appender-1.0 library installs a Logback appender that forwards log events directly to the OpenTelemetry SDK. The SDK batches them and exports via OTLP — either gRPC or HTTP — to an OpenTelemetry Collector or a compatible backend.

Pros: Simple pipeline, structured data in the OpenTelemetry log data model, trace context is attached automatically from the active span.

Cons: Adds network overhead per log batch. If the collector is unavailable, logs queue in memory up to the configured limit. Not suitable for very high-throughput services without tuning batch sizes.

Workflow 2 — File or stdout + collector filelog receiver (MDC injection)

The opentelemetry-logback-mdc-1.0 library wraps existing appenders and injects trace_id, span_id, and trace_flags into Logback’s MDC. The application writes JSON to stdout or a file, and an OpenTelemetry Collector with a filelog receiver scrapes and parses those logs.

Pros: Zero network overhead in the application process. Works with any log aggregation pipeline you already have.

Cons: Requires a parsing step downstream. If your JSON encoder is misconfigured or your regex extractor is wrong, correlation breaks silently.

Most teams running Kubernetes should prefer Workflow 2 — the collector sidecar model is already standard, and adding application-side export overhead is unnecessary. Teams with simple deployments or strict correlation requirements often find Workflow 1 easier to reason about.

Configuring Structured JSON Logs with Logback

Step 1: Dependencies

For Workflow 1 (direct OTLP export), add to your pom.xml:

<dependency>
  <groupId>io.opentelemetry.instrumentation</groupId>
  <artifactId>opentelemetry-logback-appender-1.0</artifactId>
  <version>2.11.0-alpha</version>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk</artifactId>
  <version>1.45.0</version>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
  <version>1.45.0</version>
</dependency>

For JSON formatting without the full OTLP pipeline (useful for Workflow 2), add the Logstash encoder:

<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>8.0</version>
</dependency>

If you are using Spring Boot 3.4+ and want native structured JSON without external dependencies, set this in application.properties:

logging.structured.format.console=ecs

This enables Elastic Common Schema JSON output with no extra libraries. It does not inject OpenTelemetry trace IDs automatically — for that you still need the MDC or appender approach.

Step 2: Logback configuration for Workflow 1 (OTLP export)

Create logback-spring.xml in src/main/resources:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <loggerName/>
        <message/>
        <mdc/>
        <arguments/>
        <stackTrace/>
      </providers>
    </encoder>
  </appender>

  <appender name="OpenTelemetry"
    class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
    <captureExperimentalAttributes>true</captureExperimentalAttributes>
    <captureCodeAttributes>true</captureCodeAttributes>
    <captureMarkerAttribute>true</captureMarkerAttribute>
    <captureKeyValuePairAttributes>true</captureKeyValuePairAttributes>
    <captureLoggerContext>true</captureLoggerContext>
    <captureMdcAttributes>*</captureMdcAttributes>
  </appender>

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

</configuration>

The CONSOLE appender writes JSON to stdout for local visibility. The OpenTelemetry appender forwards the same events to the SDK for OTLP export. Both run in parallel — there is no duplication of log records in the backend because the OTLP export goes to the collector, not to stdout twice.

Step 3: SDK initialization (Workflow 1)

Wire the OpenTelemetry SDK at application startup. In a Spring Boot application, do this in a @Bean or a ApplicationListener<ApplicationStartedEvent>:

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes;

@Configuration
public class OtelConfig {

  @Bean
  public OpenTelemetry openTelemetry() {
    OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter.builder()
        .setEndpoint("http://otel-collector:4317")
        .build();

    Resource resource = Resource.getDefault().toBuilder()
        .put(ResourceAttributes.SERVICE_NAME, "order-service")
        .put(ResourceAttributes.SERVICE_VERSION, "1.4.2")
        .build();

    SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
        .setResource(resource)
        .addLogRecordProcessor(
            BatchLogRecordProcessor.builder(logExporter).build())
        .build();

    OpenTelemetrySdk sdk = OpenTelemetrySdk.builder()
        .setLoggerProvider(loggerProvider)
        .build();

    // Connect the Logback appender to the initialized SDK
    OpenTelemetryAppender.install(sdk);

    return sdk;
  }
}

A critical detail that catches many teams: if OpenTelemetryAppender.install(sdk) is not called explicitly, the appender queues log events (up to numLogsCapturedBeforeOtelInstall, default 1000) and drops everything after that limit. This appears as silent log loss — no errors, no warnings, just missing logs in the backend. Always call install() during application startup before the first log event fires.

Step 4: MDC injection for Workflow 2 (stdout + filelog receiver)

For the file-based approach, replace the OpenTelemetry appender with the MDC wrapper:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <loggerName/>
        <message/>
        <mdc/>
        <arguments/>
        <stackTrace/>
      </providers>
    </encoder>
  </appender>

  <!-- MDC wrapper injects trace_id, span_id, trace_flags into MDC before encoding -->
  <appender name="OTEL_MDC"
    class="io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender">
    <appender-ref ref="CONSOLE"/>
  </appender>

  <root level="INFO">
    <appender-ref ref="OTEL_MDC"/>
  </root>

</configuration>

Add the MDC library to pom.xml:

<dependency>
  <groupId>io.opentelemetry.instrumentation</groupId>
  <artifactId>opentelemetry-logback-mdc-1.0</artifactId>
  <version>2.11.0-alpha</version>
  <scope>runtime</scope>
</dependency>

When a span is active, every log event now includes these MDC keys automatically:

MDC KeyValue
trace_idCurrent trace ID (hex string)
span_idCurrent span ID (hex string)
trace_flagsSampling flags (01 = sampled)

The JSON output for a request inside an active span looks like:

{
  "timestamp": "2025-11-18T10:23:41.123Z",
  "level": "INFO",
  "logger": "com.example.OrderService",
  "message": "Order processed",
  "trace_id": "fe9ec863c187fe62f2e674412a601165",
  "span_id": "f857539a74f125d6",
  "trace_flags": "01"
}

Logs emitted outside any active span will have empty trace_id and span_id fields. This is expected — startup logs and background thread logs that genuinely have no trace context will show empty. If you expect trace context on a log and the fields are empty, the span was either not started yet or was already closed.

OpenTelemetry Java Agent: The Simpler Path

If your team uses the OpenTelemetry Java Agent (the -javaagent approach), you do not need manual SDK initialization or the logback appender library. The agent instruments Logback automatically at JVM startup and injects trace context into MDC.

Launch your Spring Boot application with:

java -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=order-service \
  -Dotel.logs.exporter=otlp \
  -Dotel.traces.exporter=otlp \
  -Dotel.metrics.exporter=otlp \
  -Dotel.exporter.otlp.protocol=grpc \
  -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
  -jar order-service.jar

The agent intercepts Logback at the framework level and automatically:

  • Injects trace_id, span_id, and trace_flags into MDC
  • Exports log records via OTLP alongside traces and metrics
  • Adds resource attributes (service.name, host.name, runtime info)

This is the lowest-friction path for teams already running distributed tracing with the Java agent. The trade-off is reduced configuration control — for advanced cases like custom MDC keys, log filtering, or specific batch sizes, the manual SDK approach gives more control.

What the agent does NOT do automatically

One thing that trips up teams in the GitHub discussion thread on this topic: the agent does not preserve custom JSON fields from logstash-logback-encoder. The OTLP export strips the original encoder output and rebuilds the log record in the OpenTelemetry data model. Custom resource fields set in your Logback pattern or encoder do not automatically become OpenTelemetry resource attributes.

To propagate custom fields, use OpenTelemetry Resource attributes (set via OTEL_RESOURCE_ATTRIBUTES environment variable or SDK configuration), or use captureMdcAttributes=* in the appender to promote MDC keys as log record attributes.

Trace Correlation: Joining Logs to Traces

Trace correlation is the payoff for all this configuration work. When trace_id is a structured field on every log record, you can:

  1. Click a slow span in your APM trace view
  2. Jump directly to all logs emitted during that span’s lifetime
  3. See the full context: what the code was doing, what values were in play, what errors were thrown

Without structured logs with trace context, you are correlating manually by timestamp — which is imprecise and painful.

Correlation in practice

For correlation to work across your full pipeline, three things must align:

Consistent trace ID format: OpenTelemetry uses a 128-bit hex trace ID. If your APM backend stores trace IDs as 64-bit (some older Zipkin-compatible systems do), correlation will fail even though both fields are populated. Verify the format your backend expects before assuming correlation is broken.

Shared `service.name`: The backend needs to know which service emitted which logs. Set service.name consistently as an OpenTelemetry resource attribute and as a field in your JSON output. Using different names in different places breaks service-level log filtering.

Clock synchronization: Log timestamps and span start/end times must be within a few milliseconds of each other. In containerized environments, verify NTP sync across nodes. A 500ms clock skew between the application container and the collector host will make logs appear outside the span window in some UIs.

Best Practices for Spring Boot Structured Logging

Use logback-spring.xml, not logback.xml

Spring Boot’s logback-spring.xml is processed by Spring before Logback initializes, which means you can use Spring profiles and property placeholders inside the configuration. logback.xml is processed by Logback directly and cannot access Spring environment properties. For any non-trivial Spring Boot application, always use logback-spring.xml.

Never log sensitive data as structured fields

Structured logging makes data easy to query — which also makes it easy to accidentally index PII. Passwords, tokens, card numbers, and health data that appear in free-text log messages are annoying to find. The same data as a named JSON field {"card_number": "4111..."} is indexed, searchable, and a compliance violation waiting to happen. Audit your log statements for sensitive fields before enabling structured output.

Set appropriate log levels per package

A blanket INFO level on the root logger in production will produce enormous log volume from Spring framework internals, Hibernate, and connection pool events that add cost without adding signal. A practical starting configuration:

<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="com.zaxxer.hikari" level="WARN"/>
<logger name="com.example" level="INFO"/>
<root level="WARN"/>

This keeps application-level logs at INFO while suppressing framework noise.

Add business context to MDC at service boundaries

MDC fields appear on every log record emitted while the MDC entry is populated. Setting business identifiers — tenant_id, user_id, order_id — at the incoming request boundary means every log line downstream automatically carries that context without passing it through method parameters.

@Component
public class RequestContextFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    String tenantId = request.getHeader("X-Tenant-ID");
    if (tenantId != null) {
      MDC.put("tenant_id", tenantId);
    }
    try {
      chain.doFilter(req, res);
    } finally {
      MDC.clear(); // Always clear — thread pools reuse threads
    }
  }
}

The MDC.clear() in the finally block is not optional. Thread pool reuse means an MDC entry from request A will appear on request B’s logs if you forget to clear it.

Size your batch processor for your throughput

The BatchLogRecordProcessor default settings work for moderate throughput but will cause memory pressure or log loss at high volume. Key parameters to tune:

ParameterDefaultHigh-throughput recommendation
maxQueueSize20488192
maxExportBatchSize5121024
scheduleDelay1000ms200ms
exporterTimeout30000ms5000ms

Monitor the otel.sdk.log.dropped metric from the OpenTelemetry SDK — if it is non-zero, you are losing logs silently under load.

Understand log retention before committing to a backend

Structured logs are only useful if you can search them when you need them — which is often weeks after an incident. Getting this right involves understanding how different platforms handle log retention policies and the cost implications at scale.

Tools and Implementation: Backends for Spring Boot Structured Logs

Structured JSON logs need a destination that can ingest, index, and query them. The OpenTelemetry Collector is the most common intermediary — it receives OTLP or filelog input and fans out to one or more backends.

OpenTelemetry Collector configuration

A minimal collector configuration for Spring Boot logs:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 200ms
    send_batch_size: 1000
  resource:
    attributes:
      - key: environment
        value: production
        action: insert

exporters:
  otlphttp:
    endpoint: https://your-backend:4318

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [resource, batch]
      exporters: [otlphttp]

For the filelog approach (Workflow 2, stdout-based):

receivers:
  filelog:
    include: [/var/log/pods/*/*/*.log]
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.timestamp
          layout: "%Y-%m-%dT%H:%M:%S.%LZ"
      - type: move
        from: attributes.trace_id
        to: attributes["trace_id"]

CubeAPM for Spring Boot log management

CubeAPM accepts logs via the OpenTelemetry protocol directly, so any Spring Boot application configured with the OTLP exporter routes logs to CubeAPM without additional adapters. It runs inside your own VPC or on-prem infrastructure, which means log data never leaves your network — relevant for teams with data residency requirements or those running in regulated environments.

Once ingested, CubeAPM correlates logs with traces automatically using the trace_id field. In practice this means clicking a slow span in the APM trace view jumps directly to the logs emitted during that span — the correlation is a UI action, not a manual query join. The log search UI supports high-cardinality field filtering, so querying by tenant_id, order_id, or any MDC field you set is fast even at TB-scale log volumes.

Pricing is usage-based at $0.15/GB ingested with no per-seat charges and no separate indexing fees — the same rate covers ingestion, indexing, and unlimited retention. For a Spring Boot service emitting 500GB of structured logs per month, that is $75/month all-in. Datadog’s equivalent — ingest at $0.10/GB plus index at $1.70 per million log events — reaches substantially higher costs once indexing volume is factored in.

CubeAPM also supports Logstash, FluentBit, and Elastic-compatible agents, so you can point your existing log pipeline at CubeAPM without changing the Spring Boot application configuration at all.

For teams evaluating the broader landscape of log management platforms alongside CubeAPM, a comparison of top log management tools covers pricing models, search experience, and deployment options across the main options.

Grafana Loki

Loki indexes only labels (not the full log body) to keep storage costs low. For Spring Boot structured logs, you need to configure label extraction carefully — fields like service_name, level, and namespace should be labels; high-cardinality fields like trace_id and order_id should remain as log line content and queried with |= "trace_id=abc123". Loki does not support OTLP ingestion natively in most versions — you typically route through the OpenTelemetry Collector with the loki exporter.

OpenSearch / Elasticsearch

Both accept OTLP logs via the OpenTelemetry Collector’s opensearch or elasticsearch exporter. Full-text and field search are strong. The cost model is infrastructure-based (you run the cluster) or managed (AWS OpenSearch, Elastic Cloud). Index management — rolling indices, ILM policies, shard sizing — requires operational knowledge that Loki and managed backends abstract away.

Structured Logging Patterns in Other Languages

The same principles — JSON output, MDC-style context injection, OpenTelemetry trace correlation — apply across languages. If your team runs polyglot microservices, the implementation differs but the pattern is consistent. For Python services, the approach is covered in depth in the Python logging guide. Go services using slog follow a similar pattern, documented in the Go logging with slog guide.

Common Pitfalls and How to Avoid Them

Silent log loss before SDK initialization

As noted earlier, the OpenTelemetry Logback appender queues logs before install() is called (default limit: 1000 records). Spring Boot emits a large number of startup logs during bean initialization, context refresh, and auto-configuration. If your OtelConfig bean is not one of the first beans initialized, you will lose startup logs silently.

Mitigation: use a ApplicationStartingEvent listener or initialize the SDK in a static block rather than a Spring bean. Alternatively, increase numLogsCapturedBeforeOtelInstall in the appender configuration.

Custom Logback fields disappearing in OTLP export

When using the Java agent or the OTLP appender, the log record is rebuilt in the OpenTelemetry data model. Custom JSON fields added by your Logstash encoder — things like appVersion, region, or customerId — do not automatically become OpenTelemetry log attributes. They must be added via MDC (and captured with captureMdcAttributes=*) or via OpenTelemetry resource attributes.

Double-encoding in nested JSON

If you configure both a JSON encoder and the OpenTelemetry appender pointing at a console appender, you will see the log body in the OTLP export as a JSON string inside a string: "body": "{\"level\":\"INFO\",\"message\":\"...\"}". This breaks field-level querying in the backend. The OTLP appender should reference the raw console appender, not the JSON-encoded one. Treat the JSON encoder as a stdout formatting layer only — keep it separate from the OTLP export path.

MDC not cleared between requests

In any async context — @Async methods, reactive pipelines, virtual threads — MDC propagation does not happen automatically. Logback’s MDC is ThreadLocal by default. Reactive Spring (WebFlux) requires explicit context propagation using Reactor’s contextWrite and a custom MDC adapter. Without this, logs in reactive pipelines will have empty trace_id fields even when a span is active.

Structured logging in Spring Boot is not a single configuration change — it is a chain of decisions from encoder to appender to collector to backend, each of which can silently break correlation if misconfigured. The patterns above give you a working foundation: JSON output via Logstash encoder, trace context via OpenTelemetry MDC injection or OTLP appender, resource attributes for service identification, and a collector-based pipeline that keeps the application itself thin. The most important operational habit is monitoring the otel.sdk.log.dropped metric continuously — silent log loss under load is the failure mode teams discover only during incidents, when they need the logs most.

Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.

Pricing based on publicly available information as of June 2025. Enterprise discounts, custom contracts, and negotiated rates are not reflected here.

Frequently Asked Questions

What is the difference between Logback and OpenTelemetry in Spring Boot logging?

Logback is the logging framework Spring Boot uses by default — it handles log record creation, level filtering, formatting, and routing to outputs like console or file. OpenTelemetry is an observability standard that defines how telemetry data (logs, traces, metrics) is structured and transported. The two work together: Logback creates the log events, and the OpenTelemetry appender or MDC library adds trace context and routes events to an OTLP-compatible backend.

How do I enable structured JSON logging in Spring Boot 3.4 without extra libraries?

Set `logging.structured.format.console=ecs` in `application.properties`. This enables Elastic Common Schema JSON output using Spring Boot’s native structured logging support introduced in 3.4. It does not inject OpenTelemetry trace IDs automatically — for trace correlation you still need the `opentelemetry-logback-mdc-1.0` dependency or the Java agent.

Why are my trace_id and span_id fields empty in structured logs?

Empty trace context fields mean no active span existed when the log was emitted. This happens for startup logs, background threads, or async contexts where span propagation is not configured. In reactive Spring applications, MDC is thread-local and does not propagate across reactor boundaries without explicit configuration. Verify that the code emitting the log runs within an active span and that the MDC adapter supports your concurrency model.

What is the `numLogsCapturedBeforeOtelInstall` setting and why does it matter?

This setting controls how many log events the OpenTelemetry Logback appender buffers before the SDK is initialized via `OpenTelemetryAppender.install()`. The default is 1000. Spring Boot emits many logs during startup, and if SDK initialization happens after those logs fire, you lose them silently once the queue fills. Increase this limit or initialize the SDK earlier in the startup lifecycle to avoid silent log loss.

Should I use the Java agent or manual SDK initialization for log export?

The Java agent is simpler — it instruments Logback automatically with no code changes, injects trace context, and exports via OTLP. Use it if you want minimal configuration and are already running distributed tracing with the agent. Manual SDK initialization gives you more control over batch sizes, custom exporters, and resource attributes. Use it if your team has specific export requirements or needs to support multiple backends with different configurations.

How does log-trace correlation work in practice?

When a request arrives at a Spring Boot service inside an active trace, the OpenTelemetry SDK stores the current trace ID and span ID in context. The MDC injection library or OTLP appender reads that context and adds `trace_id` and `span_id` as fields on every log record emitted during that request. A compatible backend (like CubeAPM, Grafana, or OpenSearch) can then join log records to trace spans using the shared trace ID — enabling a one-click jump from a slow span to all logs generated during that span’s lifetime.

What happens to logs emitted before `OpenTelemetryAppender.install()` is called?

They are buffered up to the `numLogsCapturedBeforeOtelInstall` limit (default 1000). Once install is called, buffered logs are flushed to the SDK and exported. If the buffer fills before install is called, additional logs are silently dropped. There is no error or warning — the logs simply do not appear in the backend. This is one of the least obvious failure modes in the OpenTelemetry Logback setup and is worth testing explicitly during initial configuration.

×
×