# Correlation, propagation, and baggage

> How Elide links logs, errors, metrics, and profiles to the active span, continues W3C trace context across HTTP, and propagates W3C baggage only when you allow it.

The span active where a signal is produced links that signal to its trace. Elide applies this to everything it
records, with no code changes.

| Signal | Linked to the active span by |
| --- | --- |
| Log records | Trace id, span id, and trace flags on the record |
| Errors | An `exception` log record and span event on the span, and an `elide.errors` exemplar |
| Metrics | Exemplars on histogram buckets and sums |
| Profiles | A link from each CPU sample to its span |

Request handlers run inside the request's server span, so anything they log, throw, measure, or call joins the
request's trace.

## Logs

Every log record emitted inside a span carries its trace id, span id, and trace flags: records from the
OpenTelemetry logs API, captured `console.*` and Python `logging` output, and Elide's own log records.

```ts
// handler.ts
export default {
  async fetch(request: Request): Promise<Response> {
    console.log("looking up order"); // exported with the request span's ids
    return new Response("ok");
  },
};
```

## Errors

One error produces three records that share a timestamp and the same `exception.type`, `exception.message`, and
`exception.stacktrace`:

| Record | Where |
| --- | --- |
| Log record, severity `ERROR`, `event.name = exception` | Linked to the active span |
| `exception` event on the span, and `ERROR` status when the failure ends the span | The active span |
| `elide.errors` increment, with an exemplar pointing at the span | `error.type`, `elide.error.origin` |

With no active span, the log record and counter are still emitted. `OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN=logs` drops
the span events; the default, `logs/dup`, keeps both. Uncaught exceptions, unhandled rejections, failing request
handlers, and `span.recordException(…)` all take this path; `recordException` never changes span status.

A process that crashes cannot export. The next Elide process on the machine that has telemetry configured exports
the crash report as a `FATAL` error record: up to 16 reports per run, from the last 7 days, each sent once. See
[Emitted telemetry](https://elide.help/docs/opentelemetry/reference#log-records).

Only reports from a process with the same `service.name` are exported; a crash in a process without telemetry
configured is never exported.

## Metrics and exemplars

A counter or histogram measurement recorded inside a sampled span carries an exemplar: the trace id, span id, value,
and time. Backends that read exemplars link a latency bucket to an example trace.

| `OTEL_METRICS_EXEMPLAR_FILTER` | Exemplars from |
| --- | --- |
| `trace_based` (default) | Measurements inside a sampled span |
| `always_on` | Every measurement |
| `always_off` | None |

Elide keeps the latest exemplar per histogram bucket and per sum in each export. `http.server.request.duration` and
`http.client.request.duration` carry exemplars of their own request spans.

## Profiles

Each guest CPU sample links to the span active on the sampled thread when the sample was taken, including after
`await`, in promise callbacks, and across Python asyncio tasks. See [Profiles](https://elide.help/docs/opentelemetry/signals#profiles).

## Trace context over HTTP

Elide's HTTP server and clients use W3C Trace Context in both directions:

| Direction | Behavior |
| --- | --- |
| Inbound | A valid `traceparent` (with `tracestate`) on a served request becomes the parent of the server span. An invalid one starts a new trace. |
| Outbound | `fetch` sends `traceparent` and `tracestate` for its client span. A header the caller set is never replaced. |

```bash
curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' http://localhost:8080/
```

`traces.propagators` (or `OTEL_PROPAGATORS`) chooses what `propagation.inject` and `propagation.extract` do in your
code: `tracecontext`, `baggage`, or `none`. Removing `tracecontext` also stops Elide's HTTP clients from sending
`traceparent`. `node:http` client requests and Python HTTP clients do not send it yet; inject it with the API:

```ts
import { context, propagation } from "@opentelemetry/api";
import http from "node:http";

const headers: Record<string, string> = {};
propagation.inject(context.active(), headers);
http.get("http://localhost:8081/inventory", { headers });
```

## Baggage

W3C baggage carries key-value pairs alongside a trace. Your code can always read, set, and send baggage through the
OpenTelemetry API. Elide's HTTP server and clients pass the `baggage` header only for directions you turn on, because
baggage from a client is untrusted input:

```pkl
opentelemetry {
  propagation {
    baggage {
      inbound = true
      outbound = true
      allow { "tenant"; "region" }
      spanAttributes { "tenant" }
    }
  }
}
```

| Property | Variable | Effect | Default |
| --- | --- | --- | --- |
| `inbound` | `ELIDE_OTEL_BAGGAGE_INBOUND` | A served request's `baggage` header becomes the handler's baggage. | `false` |
| `outbound` | `ELIDE_OTEL_BAGGAGE_OUTBOUND` | `fetch` sends the current baggage, unless the request sets `baggage` itself. | `false` |
| `allow` | `ELIDE_OTEL_BAGGAGE_ALLOW` | Only these keys pass, in both directions. | all keys |
| `spanAttributes` | — | These inbound keys are recorded on the server span as `baggage.<key>`; each must be in `allow`. | none |

- A header over 8,192 bytes or 180 members, or with any malformed member, is dropped whole.
- Baggage received on a request reaches outgoing requests only when both directions are on.
- `OTEL_PROPAGATORS` does not turn baggage on for Elide's HTTP server and clients; it affects only the API in your
  code.

Read inbound baggage in a handler:

```ts
import { context, propagation } from "@opentelemetry/api";

export default {
  async fetch(): Promise<Response> {
    const tenant = propagation.getBaggage(context.active())?.getEntry("tenant")?.value;
    return new Response(`tenant: ${tenant ?? "none"}\n`);
  },
};
```

## Samples

- [Baggage across services](https://elide.help/docs/code-samples#baggage-across-services)
