# `Impact.Propagator`

Cross-process OpenTelemetry context propagation.

The BEAM has no shared memory between processes, so the OTel context (the
active span + baggage from `Impact.Context.put/1`) does **not** automatically
travel across `GenServer.call/3`, `Task.async/1`, or Oban job boundaries. If
you want the spans on the other side to be children of the calling trace
instead of orphans, you have to carry the context explicitly.

This module wraps the common patterns.

## GenServer.call across a process boundary

    # Caller side
    def chat(session_id, message) do
      ctx = Impact.Propagator.current()
      GenServer.call(via(session_id), {:chat, message, ctx})
    end

    # GenServer side
    def handle_call({:chat, message, ctx}, _from, state) do
      Impact.Propagator.attach(ctx)
      Impact.trace [type: :task, name: "agent.loop"] do
        ...
      end
    end

## Task.start with context

    ctx = Impact.Propagator.current()

    Task.start(fn ->
      Impact.Propagator.with_ctx(ctx, fn ->
        do_async_work()
      end)
    end)

## Oban jobs

    # Enqueue side — inject W3C textmap into job args
    args = Map.put(args, :_otel, Impact.Propagator.inject())
    MyWorker.new(args) |> Oban.insert()

    # Worker side — extract on perform
    def perform(%Oban.Job{args: args}) do
      Impact.Propagator.extract_and_attach(args["_otel"])
      ...
    end

## Why two carrier formats

Two carrier formats are exposed, fit for different transports:

  * **Opaque context term** (`current/0`, `attach/1`) — cheap, no encoding
    cost, but only safe to pass via a BEAM message or capture in a closure
    that runs in the same node.
  * **W3C textmap** (`inject/0`, `extract_and_attach/1`) — `traceparent` +
    `tracestate` HTTP-header-style strings, serializable into JSON / DB
    columns / Oban job args / outbound HTTP headers.

# `attach`

```elixir
@spec attach(:otel_ctx.t()) :: :ok
```

Attach `ctx` to the current process. Returns the previous context token,
which the caller can discard or pass to `OpenTelemetry.Ctx.detach/1` to
restore later.

Note: `attach` does not restore the previous context on its own. Use
`with_ctx/2` if you want auto-restore semantics (recommended inside `Task`).

# `current`

```elixir
@spec current() :: :otel_ctx.t()
```

Return the current OTel context (opaque term).

# `extract_and_attach`

```elixir
@spec extract_and_attach(map() | nil) :: :ok
```

Extract OTel context from a textmap (e.g. an Oban job arg map) and install
it on the current process. No-op when the input is nil or empty.

# `inject`

```elixir
@spec inject() :: %{optional(String.t()) =&gt; String.t()}
```

Serialize the current OTel context as W3C textmap headers. Returns a plain
string-keyed map containing `"traceparent"` (and optionally `"tracestate"`),
suitable for storing in Oban job args, JSON columns, or outbound HTTP
headers.

Returns `%{}` when no context is active.

# `with_ctx`

```elixir
@spec with_ctx(:otel_ctx.t(), (-&gt; any())) :: any()
```

Run `fun` inside `ctx`, restoring the previous context on exit (even if
`fun` raises). Preferred pattern for `Task.start/Task.async` closures
because it prevents context leakage into long-lived processes.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
