LangChain.OpenTelemetry.Enrich (LangChain v0.9.5)

Copy Markdown View Source

Supported way to add application context to the OpenTelemetry spans LangChain creates.

Safe to call unconditionally. Unlike LangChain.OpenTelemetry — which only exists when the optional :opentelemetry_api dependency is present — every function here compiles to a no-op when OpenTelemetry is unavailable. Callers need neither their own Code.ensure_loaded?/1 guards nor an OpenTelemetry dependency of their own.

Which to reach for

Most callers want neither of these. Setting custom_context[:otel_attributes] on the chain covers the common case, and covers it better, because those attributes are applied when each span opens rather than after the fact:

chain
|> LLMChain.update_custom_context(%{
  otel_attributes: %{"user.id" => user.id, "organization.id" => org.id}
})

Use this module for the two things that map cannot do.

set_current_span_attributes/1 — enrich from inside a running operation

For values only known once the work is underway: a resolved record id, a classification the tool computed, a cache hit or miss.

# inside a tool function, or an :on_tool_execution_completed callback
Enrich.set_current_span_attributes(%{"myapp.records_matched" => length(rows)})

Which span it lands on depends on where you call it, because it always targets the innermost open span:

Called fromSpan it enriches
A tool's own function bodyexecute_tool {tool}
:on_tool_pre_execution, :on_tool_execution_completed, :on_tool_execution_failedexecute_tool {tool}
:on_message_processed, :on_llm_token_usageinvoke_agent {chain_type}
Outside any LangChain operationWhatever span your app has open, or nothing

Attributes set this way apply to that one span. They are not inherited by sibling or later spans — use put_inherited_attributes/1 for that.

put_inherited_attributes/1 — seed context before the chain runs

For hosts that establish request context outside LangChain entirely (a Plug, a LiveView mount/3, an Oban worker) and want every LangChain span in that process to carry it, without threading a chain through their code:

# in a Plug, before any chain exists
Enrich.put_inherited_attributes(%{"organization.id" => conn.assigns.org.id})

These ride the OpenTelemetry context, so they reach every span LangChain opens afterwards in that process, and follow the trace across process boundaries wherever the context is propagated. They are never serialized onto outbound requests the way baggage is.

Values

Both functions coerce values through LangChain.OpenTelemetry.Attributes.attribute_value/1. Strings, numbers, booleans, and homogeneous lists of those stay native; anything else is JSON-encoded. nil values are dropped. This matters more than it looks: an uncoerced nested map makes the SDK raise, the span handler traps the exception, and the span silently vanishes from the trace. Since this module is the one place arbitrary caller data reaches span attributes, it never skips coercion.

Summary

Functions

Seeds attributes that every LangChain span opened later in this process will inherit.

Sets attributes on the currently-active span.

Functions

put_inherited_attributes(attrs)

@spec put_inherited_attributes(map() | keyword()) :: :ok

Seeds attributes that every LangChain span opened later in this process will inherit.

Merges with anything already seeded, with the new values winning. Returns :ok always.

Note this attaches a new OpenTelemetry context to the current process and does not detach it — appropriate for request-scoped setup code (a Plug, a LiveView mount, a job's perform/1), where the process ends with the request. Do not call it in a loop in a long-lived process.

Example

# in a Plug
LangChain.OpenTelemetry.Enrich.put_inherited_attributes(%{
  "organization.id" => conn.assigns.current_org.id,
  "myapp.plan" => conn.assigns.current_org.plan
})

set_current_span_attributes(attrs)

@spec set_current_span_attributes(map() | keyword()) :: :ok

Sets attributes on the currently-active span.

Returns :ok always, including when OpenTelemetry is unavailable or no span is open — enrichment is never worth failing a request over.

Example

LangChain.OpenTelemetry.Enrich.set_current_span_attributes(%{
  "myapp.cache" => "hit",
  "myapp.rows" => 42
})