# `LangChain.OpenTelemetry.Attributes`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L1)

Builds OpenTelemetry span attribute maps from LangChain telemetry metadata,
following a subset of the GenAI Semantic Conventions (v1.40+).

Attribute key constants are defined as string literals because the Hex
`opentelemetry_semantic_conventions` package lags behind the latest spec.

## Coverage

This integration emits the following semantic-convention attributes:

  * `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.output.type`
    (`"json"` when the model requests structured output, else `"text"`)
  * `gen_ai.request.model`, `gen_ai.response.model`
  * `server.address`, `server.port` (derived from the model's request endpoint)
  * Request parameters (when the model sets them):
    `gen_ai.request.temperature`, `gen_ai.request.max_tokens`,
    `gen_ai.request.top_p`, `gen_ai.request.top_k`,
    `gen_ai.request.frequency_penalty`, `gen_ai.request.presence_penalty`,
    `gen_ai.request.seed`, `gen_ai.request.choice.count`,
    `gen_ai.request.stream`, `gen_ai.request.stop_sequences`,
    `gen_ai.request.reasoning.level`
  * `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and — best-effort
    from the provider-specific `TokenUsage.raw` — `gen_ai.usage.cache_read.input_tokens`,
    `gen_ai.usage.cache_creation.input_tokens`, `gen_ai.usage.reasoning.output_tokens`
  * `gen_ai.response.finish_reasons` (best-effort from `Message.status`)
  * `gen_ai.input.messages`, `gen_ai.output.messages` (opt-in — see `Config`)
  * `gen_ai.tool.name`, `gen_ai.tool.call.id`, `gen_ai.tool.type`,
    `gen_ai.tool.description`,
    `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` (opt-in)
  * `gen_ai.agent.name`, `gen_ai.agent.id`, `gen_ai.conversation.id` (from `custom_context`)
  * `error.type` (on failed operations)
  * Any attributes the caller supplies via `custom_context[:otel_attributes]` —
    see `passthrough_attributes/1`

It does **not** currently emit `gen_ai.response.id` or a distinct
`gen_ai.response.model` (LangChain keeps no response id and normalizes the
response model to the requested one). Treat the output as a useful subset
rather than full conformance.

Streaming time-to-first-token *is* captured, but by
`LangChain.OpenTelemetry.SpanHandler` (as a `gen_ai.response.time_to_first_token`
attribute and a `gen_ai.first_token` span event) rather than here, because it is
derived from a streaming lifecycle event rather than the call metadata this
module maps.

See: https://opentelemetry.io/docs/specs/semconv/gen-ai/

# `attribute_value`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L693)

```elixir
@spec attribute_value(term()) :: term()
```

Coerces a caller-supplied value into something safe to set as a span attribute.

Values the OpenTelemetry spec supports natively — strings, integers, floats,
booleans, and homogeneous lists of those — pass through **unchanged**, so numeric
attributes stay numeric and remain filterable as numbers in the backend. Anything
else is JSON-encoded, falling back to `inspect/1` when it cannot be encoded.

This exists because an uncoerced value is not a loud failure. A nested map has no
`String.Chars` implementation, so the SDK's stringification raises; the span
handler traps that exception (it must, since a raising `:telemetry` handler is
detached VM-wide) and the result is that **the whole span silently disappears**.
Every caller-supplied value must pass through here.

`nil` returns `nil` so callers can drop the attribute entirely; there is no
meaningful "null" span attribute.

# `chain_start`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L433)

```elixir
@spec chain_start(map()) :: [{String.t(), term()}]
```

Builds attributes for a chain execution event.

Sets `gen_ai.conversation.id`, `gen_ai.agent.name`, and `gen_ai.agent.id` from
`custom_context`, extracts Langfuse-specific attributes when present, and applies
the caller's `:otel_attributes` passthrough (see `passthrough_attributes/1`).

# `chain_stop`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L489)

```elixir
@spec chain_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [{String.t(), term()}]
```

Builds attributes for a chain execution stop event.

Extracts the first user message as input and the last assistant message as output
so they appear on the trace-level span in Langfuse (and other OTEL backends).

# `custom_context_attributes`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L556)

```elixir
@spec custom_context_attributes(map() | nil) :: [{String.t(), term()}]
```

Extracts Langfuse-specific attributes from a `custom_context` map.

Supported keys:
- `:langfuse_trace_name` -> `langfuse.trace.name`
- `:langfuse_user_id` -> `langfuse.user.id`
- `:langfuse_session_id` -> `langfuse.session.id`
- `:langfuse_tags` -> `langfuse.trace.tags`
- `:langfuse_metadata` -> `langfuse.trace.metadata.*` (flattened)

# `llm_call_start`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L107)

```elixir
@spec llm_call_start(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]
```

Builds attributes for an LLM call start event.

Returns operation name, output type, model, provider, and request-parameter
attributes (`gen_ai.request.*`, sourced from `metadata[:request_options]`).
Input message capture is handled separately by the prompt event handler in
`SpanHandler`.

# `llm_call_stop`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L195)

```elixir
@spec llm_call_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]
```

Builds attributes for an LLM call stop event (token usage and response model).

When `config.capture_output_messages` is true and `metadata[:result]` contains
a message, serializes output messages into `gen_ai.output.messages`.

# `merge`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L743)

```elixir
@spec merge(list(), list()) :: list()
```

Merges two attribute lists, with `override` winning on duplicate keys.

Attribute lists are keyword-like lists of `{key, value}` tuples, and the SDK's
own de-duplication order is an implementation detail we should not depend on.
Resolving collisions here keeps precedence explicit and testable at the two
places it matters:

  * caller-supplied `:otel_attributes` override LangChain-derived values on the
    same span, because a caller who names a key means it;
  * inherited attributes (see `LangChain.OpenTelemetry.SpanHandler`) **lose** to
    the values an event derives, so an inherited `gen_ai.request.model` can never
    overwrite the real per-call model on a `chat` span.

# `operation_name_key`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L95)

Returns the `gen_ai.operation.name` attribute key.

# `passthrough_attributes`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L657)

```elixir
@spec passthrough_attributes(map() | nil) :: [{String.t(), term()}]
```

Extracts caller-supplied span attributes from `custom_context[:otel_attributes]`.

`:otel_attributes` is a **reserved key** in `LLMChain.custom_context`: a flat map
of attribute name to value that LangChain copies onto the spans it creates.

    chain =
      %{llm: llm, messages: messages}
      |> LLMChain.new!()
      |> Map.put(:custom_context, %{
        otel_attributes: %{
          "user.id" => user.id,
          "organization.id" => org.id,
          "myapp.feature" => "support_chat"
        }
      })

The key is reserved and explicit rather than LangChain forwarding all of
`custom_context`, because `custom_context` routinely holds structs, PIDs, and
whole conversation states. Copying it wholesale would produce enormous spans and
leak data the caller never intended to export.

Keys may be strings or atoms; values are coerced by `attribute_value/1`. Entries
with an unusable key or a `nil` value are dropped rather than exported as empty
attributes.

Namespace application-specific names (`myapp.*`) per the semantic conventions. A
`gen_ai.*` key is honoured deliberately, so a caller can fill a convention slot
LangChain does not populate itself.

# `tool_call`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L355)

```elixir
@spec tool_call(map(), LangChain.OpenTelemetry.Config.t()) :: [{String.t(), term()}]
```

Builds attributes for a tool call start event.

When `config.capture_tool_arguments` is true and `metadata[:arguments]` is present,
serializes arguments into `gen_ai.tool.call.arguments`. Also applies the caller's
`:otel_attributes` passthrough (see `passthrough_attributes/1`), which reaches tool
spans because `LLMChain` includes `custom_context` in the tool-call telemetry
metadata.

# `tool_call_stop`
[🔗](https://github.com/brainlid/langchain/blob/v0.15.3/lib/open_telemetry/attributes.ex#L408)

```elixir
@spec tool_call_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]
```

Builds attributes for a tool call stop event.

When `config.capture_tool_results` is true and `metadata[:tool_result]` is present,
extracts the result content into `gen_ai.tool.call.result`.

---

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