# `ReqLLM.OpenTelemetry`
[🔗](https://github.com/agentjido/req_llm/blob/main/lib/req_llm/open_telemetry.ex#L345)

Bridges ReqLLM request lifecycle telemetry into OpenTelemetry GenAI client spans.

This module listens to the existing `[:req_llm, :request, *]` events and emits a
single client span per model call. The span follows the OpenTelemetry Generative AI
semantic conventions where ReqLLM has normalized data available, including:

- `gen_ai.provider.name`
- `gen_ai.operation.name`
- `gen_ai.request.model`
- `gen_ai.output.type`
- `gen_ai.response.finish_reasons`
- `gen_ai.usage.*`
- `error.type`

Span export remains opt-in at the application level. You still need OpenTelemetry
dependencies and SDK/exporter configuration in your host app. When the OpenTelemetry
API modules are not available, `attach/2` returns `{:error, :opentelemetry_unavailable}`.

For custom tracer integrations that want richer message and tool-call mapping
without binding ReqLLM to a specific OpenTelemetry SDK, see
`ReqLLM.Telemetry.OpenTelemetry`.

# `attach_opt`

```elixir
@type attach_opt() ::
  {:adapter, module()}
  | {:handler_id, term()}
  | {:content, content_mode() | boolean()}
  | {:langfuse, boolean()}
  | {atom(), term()}
```

# `content_mode`

```elixir
@type content_mode() :: :none | :attributes | :event
```

# `attach`

```elixir
@spec attach(term(), keyword()) ::
  :ok | {:error, :already_exists | :opentelemetry_unavailable}
```

Attaches the OpenTelemetry bridge to ReqLLM request lifecycle events.

Options:

- `:adapter` — alternate adapter module (defaults to
  `ReqLLM.OpenTelemetry.OTelAdapter`).
- `:content` — content capture mode. `:none` (default) emits no message
  payloads; `:attributes` promotes `gen_ai.input.messages`,
  `gen_ai.system_instructions`, `gen_ai.tool.definitions`, and
  `gen_ai.output.messages` onto the span; `:event` emits the same payload
  as a single `gen_ai.client.inference.operation.details` span event on
  the terminal lifecycle event. `true` is accepted as an alias for
  `:attributes`.
- `:langfuse` — when `true`, adds Langfuse-specific attributes the regular
  `gen_ai.*` set doesn't cover:
    * `langfuse.observation.cost_details` — JSON-encoded `input` / `output` /
      `reasoning` / `total` breakdown, when ReqLLM has computed a cost.
    * `langfuse.observation.completion_start_time` — ISO 8601 timestamp of
      the first streamed chunk, used by Langfuse to compute time-to-first-
      token. Only set for streaming requests that produced at least one
      chunk.

Content capture additionally requires `telemetry: [payloads: :raw]` on the
call so the request/response payloads are available to map.

In-flight spans are tracked in a named ETS table keyed by handler id and
request id. If a `:start` event is observed but no `:stop`/`:exception`
follows (e.g. the calling process crashed before emission), the entry stays
in the table until `detach/1` runs. Long-running hosts can call
`prune_stale_spans/2` periodically to clear out entries older than a TTL.

# `available?`

```elixir
@spec available?(keyword()) :: boolean()
```

Returns whether the configured OpenTelemetry adapter is available.

# `detach`

```elixir
@spec detach(term()) :: :ok
```

Detaches the OpenTelemetry bridge and clears any in-flight spans for the handler.

# `events`

```elixir
@spec events() :: [[atom()]]
```

Returns the request lifecycle events used by the bridge.

# `prune_stale_spans`

```elixir
@spec prune_stale_spans(term(), non_neg_integer()) :: non_neg_integer()
```

Removes in-flight span entries older than `ttl_ms` for `handler_id`.

Returns the number of entries pruned. Use this from a host-side scheduler
(e.g. an `:erlang.send_after/3` loop or a periodic GenServer tick) to
contain the ETS table when requests start without a matching stop or
exception event.

A supervised storage process owns the table for the application.
If that owner restarts, active span records and cached instruments are cleared.
Terminal events for cleared spans are ignored. New requests start new spans.

# `span_name`

```elixir
@spec span_name(map()) :: String.t()
```

Returns the GenAI span name for a ReqLLM request.

---

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