# Trace-log correlation

> Learn how OBI correlates application logs with distributed traces for faster debugging and troubleshooting.

---

LLMS index: [llms.txt](/llms.txt)

---

OpenTelemetry eBPF Instrumentation (OBI) correlates application logs with
distributed traces by enriching JSON and plain-text logs with trace context. OBI
does not export logs; it writes enriched logs back to the same stream while
traces are exported via OTLP.

## Overview

Trace-log correlation connects two complementary observability signals:

- **Traces**: Show the flow of requests across services with timing and
  structure
- **Logs**: Provide detailed event information and application state

With OBI trace-log correlation, logs from instrumented processes are
automatically enriched with trace context:

- **Trace ID**: Links a log entry to the distributed trace
- **Span ID**: Links a log entry to a specific trace span

This enables your observability backend to correlate logs with their originating
traces without any code changes to your application.

## How it works

OBI uses eBPF to inject trace context into application logs at the kernel level:

1. **Trace capturing**: OBI captures trace context (trace ID and span ID) for
   all traced operations
2. **Log interception**: OBI intercepts write syscalls to capture application
   logs
3. **Context injection**: OBI injects `trace_id` and `span_id` fields into JSON
   objects or adds configurable `key=value` fields to selected plain-text lines
4. **Trace export**: Logs keep flowing through your existing logging pipeline
5. **Backend linking**: Your observability backend links logs to traces using
   these IDs

### Technical approach

OBI performs correlation at the kernel level without modifying application
binaries:

- Uses kernel eBPF probes to intercept write operations
- Maintains file descriptor caching for performance
- Works with logging frameworks that write JSON or plain text

OBI preserves configured trace and span fields that already exist. JSON keys are
matched literally; in plain text, OBI recognizes `name=value` tokens at the
start of a line or after whitespace. For a service detected as exporting
OpenTelemetry traces directly, OBI injects only `trace_id`: its eBPF-generated
span ID would not identify the SDK span.

## Configuration

To correlate logs with traces, export traces and configure OBI to add trace
context to logs from the selected workloads. The configuration fields differ
between Config v1 and Config v2. If you use Config v2, see the
[Config v2 reference](../configure/config-v2/). To convert an existing Config v1
file, follow the [migration guide](../configure/migrate-to-config-v2/).

### Config v1

```yaml
# Enable trace export
otel_traces_export:
  endpoint: http://otel-collector:4318/v1/traces

# Select services to instrument
discovery:
  instrument:
    - open_ports: '8380'

# Enable log enrichment for the same services
ebpf:
  log_enricher:
    services:
      - service:
          - open_ports: '8380'
```

Log enrichment behavior can be further configured under `ebpf.log_enricher`:

- `cache_ttl`: time-to-live for cached file descriptors
- `cache_size`: maximum number of cached file descriptors
- `async_writer_workers`: number of async writer shards
- `async_writer_channel_len`: queue size per shard
- `field_names`: field names used to recognize and inject trace and span IDs
- `plain_text.enabled`: whether to annotate non-JSON logs; defaults to `true`
- `plain_text.placement`: add fields as a `prefix` or `suffix`
- `plain_text.multiline`: annotate the `first_line`, `last_line`, or `each_line`
  in each intercepted write

For example:

```yaml
ebpf:
  log_enricher:
    field_names:
      trace_id: trace_id
      span_id: span_id
    plain_text:
      enabled: true
      placement: suffix
      multiline: first_line
```

Plain-text enrichment is enabled by default for selected services in v0.11.0.
Set `plain_text.enabled: false` before upgrading if non-JSON writes must retain
the earlier pass-through behavior. Field names apply to JSON and plain-text
output and must be nonempty, distinct, and contain no whitespace, `=`, or
control characters.

#### Service selection

OBI enriches JSON and plain-text logs for services listed under
`ebpf.log_enricher.services`. Keep service selectors aligned with
`discovery.instrument` so enrichment tracks the same processes.

### Config v2

With Config v2, log trace annotation is available only when you run OBI as a
standalone process. It is disabled by default. Configure it under
`extensions.obi.correlation.log_trace_annotation`:

```yaml
extensions:
  obi:
    correlation:
      log_trace_annotation:
        enabled: true
        field_names:
          trace_id: trace_id
          span_id: span_id
        plain_text:
          enabled: true
          placement: suffix
          multiline: first_line
```

Config v2 capture selection determines which workloads are eligible for log
annotation. The `log_trace_annotation.filter` field is reserved in v0.11.0 and
must remain empty.

## Requirements

### 1. Supported log format

For JSON-formatted logs, OBI injects `trace_id` and `span_id` fields into JSON
objects:

**Before OBI**:

```json
{ "level": "info", "message": "Request processed", "duration_ms": 125 }
```

**After OBI enrichment**:

```json
{
  "level": "info",
  "message": "Request processed",
  "duration_ms": 125,
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}
```

For plain-text logs, OBI adds lowercase, fixed-width IDs as space-separated
`key=value` fields. Placement and multiline selection are configurable:

```text
request processed trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7
```

Newline-delimited JSON is handled as structured JSON. OBI enriches each object
record independently and doesn't apply plain-text annotation to valid NDJSON.
Multiline selection operates on nonempty physical lines in one intercepted
write; OBI doesn't reconstruct logical events across separate writes.

#### Runtime buffering limitations

The log enricher only sees trace context when the log write happens on the
request-handling thread. Runtimes that buffer stdout asynchronously can break
this assumption.

- Python in Docker commonly needs `PYTHONUNBUFFERED=1`
- .NET `Console.Out` is buffered by default when stdout is a pipe; use a
  `StreamWriter` with `AutoFlush = true`
- ASP.NET Core's default `Microsoft.Extensions.Logging.AddConsole()` pipeline is
  not compatible because it writes from a background thread
- Java virtual-thread logs aren't enriched because a carrier kernel thread can
  execute work from multiple virtual threads; platform-thread enrichment is
  unaffected

### 2. Trace export and log enrichment enabled

Trace-log correlation requires both trace export and log enrichment. For Config
v1:

```yaml
otel_traces_export:
  endpoint: http://collector:4318/v1/traces # Required

ebpf:
  log_enricher:
    services:
      - service:
          - open_ports: '8380' # Required
```

### 3. Linux kernel

Trace-log correlation requires Linux with specific kernel features:

- **Linux kernel 6.0+** (required for trace-log correlation)
- Supported architectures: x86_64, ARM64
- **BPFFS mount**: The kernel must have BPF filesystem mounted at `/sys/fs/bpf`
- **Non-security-locked-down kernel**: Requires a kernel that is not running in
  security lockdown mode (typical for most production distributions)

### 4. Framework that emits supported logs

Applications can use a logging framework configured to output JSON or plain
text. The following JSON examples produce structured fields:

     <ul class="nav nav-tabs" id="tabs-0" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-00-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-00-00" role="tab"
          data-td-tp-persist="python" aria-controls="tabs-00-00" aria-selected="true">
        Python
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-00-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-00-01" role="tab"
          data-td-tp-persist="go" aria-controls="tabs-00-01" aria-selected="false">
        Go (using zap)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-00-02-tab" data-bs-toggle="tab" data-bs-target="#tabs-00-02" role="tab"
          data-td-tp-persist="java" aria-controls="tabs-00-02" aria-selected="false">
        Java (using Logback)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-00-03-tab" data-bs-toggle="tab" data-bs-target="#tabs-00-03" role="tab"
          data-td-tp-persist="javascript" aria-controls="tabs-00-03" aria-selected="false">
        Node.js (using pino)
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-0-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-00-00" role="tabpanel" aria-labelled-by="tabs-00-00-tab" tabindex="0">
        <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">JSONFormatter</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">Formatter</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">format</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">record</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">log_entry</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;timestamp&#39;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">formatTime</span><span class="p">(</span><span class="n">record</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;level&#39;</span><span class="p">:</span> <span class="n">record</span><span class="o">.</span><span class="n">levelname</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;message&#39;</span><span class="p">:</span> <span class="n">record</span><span class="o">.</span><span class="n">getMessage</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;module&#39;</span><span class="p">:</span> <span class="n">record</span><span class="o">.</span><span class="n">module</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">log_entry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">handler</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">StreamHandler</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">handler</span><span class="o">.</span><span class="n">setFormatter</span><span class="p">(</span><span class="n">JSONFormatter</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">addHandler</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-00-01" role="tabpanel" aria-labelled-by="tabs-00-01-tab" tabindex="0">
        <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="s">&#34;go.uber.org/zap&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">logger</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">NewProduction</span><span class="p">()</span><span class="w"> </span><span class="c1">// Outputs JSON by default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">defer</span><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Sync</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;Request processed&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="s">&#34;duration&#34;</span><span class="p">,</span><span class="w"> </span><span class="mi">125</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">))</span><span class="w">
</span></span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-00-02" role="tabpanel" aria-labelled-by="tabs-00-02-tab" tabindex="0">
        <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;appender</span> <span class="na">name=</span><span class="s">&#34;FILE&#34;</span> <span class="na">class=</span><span class="s">&#34;ch.qos.logback.core.ConsoleAppender&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;encoder</span> <span class="na">class=</span><span class="s">&#34;net.logstash.logback.encoder.LogstashEncoder&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/appender&gt;</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-00-03" role="tabpanel" aria-labelled-by="tabs-00-03-tab" tabindex="0">
        <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">pino</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;pino&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">logger</span> <span class="o">=</span> <span class="nx">pino</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">({</span> <span class="nx">duration_ms</span><span class="o">:</span> <span class="mi">125</span> <span class="p">},</span> <span class="s1">&#39;Request processed&#39;</span><span class="p">);</span>
</span></span></code></pre></div>
    </div>
</div>


### 5. Log shipping pipeline

OBI enriches logs in-place. Use your existing log forwarder or collector to ship
logs to your backend.

When OBI suppresses the original line, container log files contain a line of NUL
bytes in its place. For writes up to 8 KiB, filter these placeholder lines
downstream with `^[\x00\s]*$`. For example, with the OpenTelemetry Collector
`file_log` receiver:

```yaml
receivers:
  file_log:
    include:
      - /var/log/pods/*/*/*.log
    start_at: end
    operators:
      - type: container
      - type: filter
        expr: 'body matches "^[\\x00\\s]*$"'
```

CRI and Docker JSON log envelopes encode NUL as `\u0000`; the `container`
operator decodes the body before the filter runs.

## Performance considerations

- **Minimal overhead**: Correlation uses eBPF kernel probes with efficient file
  descriptor caching
- **Cache limits**: File descriptor cache has size and TTL limits to prevent
  unbounded memory usage
- **Async processing**: Log enrichment uses asynchronous workers to avoid
  overflowing the kernel ringbuffer

## Known limitations

- **Per-write multiline selection**: OBI doesn't reconstruct logical multiline
  events across separate writes
- **File descriptor cache**: Cached for performance, with configurable TTL
  (default: 30 minutes)
- **Span-aligned only**: Logs enriched only while a span is active; logs outside
  span scope are not enriched.
- **8 KiB per-write limit**: OBI enriches and suppresses at most the first 8 KiB
  of a single `write()` or `writev()`. Any remaining bytes pass through
  un-enriched and don't match the placeholder-line filter.
- **Java virtual threads**: Logs written from virtual threads aren't enriched.

## Troubleshooting

### Trace context not appearing in logs

1. **Verify the configured format**: For JSON logs, ensure the application
   outputs valid JSON. For plain text, confirm `plain_text.enabled` is `true`
   and inspect the placement and multiline settings.

   ```bash
   # Check for malformed JSON
   cat app.log | jq empty && echo "Valid JSON" || echo "Invalid JSON"
   ```

2. **Verify trace export and log enrichment**:

   ```yaml
   otel_traces_export:
     endpoint: http://collector:4318/v1/traces

   ebpf:
     log_enricher:
       services:
         - service:
             - open_ports: '8380'
   ```

3. **Verify Linux kernel**: Trace-log correlation requires Linux

   ```bash
   uname -s  # Must return "Linux"
   ```

4. **Check log pipeline**: Verify your log forwarder is shipping logs to your
   backend

## What's next?

- Set up [export destinations](/docs/zero-code/obi/configure/export-data/) for
  traces and metrics
- Explore OBI
  [as a Collector receiver](/docs/zero-code/obi/configure/collector-receiver/)
  for centralized processing
