OpenTelemetry integration for LangChain.
Attaches to LangChain's :telemetry events and translates them into
OpenTelemetry spans and metrics following a subset of the
GenAI Semantic Conventions (v1.40+).
See LangChain.OpenTelemetry.Attributes for exactly which attributes are (and are not) emitted.
Setup
Add the optional dependencies to your mix.exs:
{:opentelemetry_api, "~> 1.4"},
{:opentelemetry, "~> 1.5"},
{:opentelemetry_exporter, "~> 1.8"}Then call setup/1 in your application startup (e.g., Application.start/2):
LangChain.OpenTelemetry.setup()Options
Options are passed through to LangChain.OpenTelemetry.Config:
:capture_input_messages- Record input messages as span attributes. Default:false. When enabled, serializesLangChain.Messagestructs sent to the LLM intogen_ai.input.messagesas a JSON string. May contain sensitive data.:capture_output_messages- Record output messages as span attributes. Default:false. When enabled, serializes the LLM response intogen_ai.output.messages. May contain sensitive data.:capture_tool_arguments- Record tool call arguments. Default:false. Serializes tool call arguments intogen_ai.tool.call.arguments.:capture_tool_results- Record tool call results. Default:false. Captures tool return values intogen_ai.tool.call.result.:enable_metrics- Re-emit operation-duration and token-usage metric events. Default:true. See the note below.
Metrics require a consumer
:enable_metrics does not record OpenTelemetry histograms or counters
on its own. It re-emits LangChain telemetry as the intermediary events
[:langchain, :otel, :operation, :duration] and [:langchain, :otel, :token, :usage].
To turn these into real metrics, attach a consumer such as Telemetry.Metrics
(with an OpenTelemetry reporter), PromEx, or equivalent. Without a consumer,
enable_metrics: true has no observable effect. See
LangChain.OpenTelemetry.MetricsHandler for the emitted event shapes.
Span Mapping
| Telemetry Event | OTel Span Name | Kind |
|---|---|---|
[:langchain, :chain, :execute, ...] | invoke_agent llm_chain | :internal |
[:langchain, :llm, :call, ...] | chat {model} | :client |
[:langchain, :tool, :call, ...] | execute_tool {tool_name} | :internal |
Langfuse Integration
Langfuse can ingest OpenTelemetry traces via its OTLP-compatible endpoint. Configure the OTel exporter to point at your Langfuse instance:
# config/runtime.exs
config :opentelemetry_exporter,
otlp_protocol: :http_protobuf,
otlp_endpoint: "https://your-langfuse-host/api/public/otel",
otlp_headers: [
{"Authorization", "Basic " <> Base.encode64("pk-lf-...:sk-lf-...")}
]Langfuse-specific attributes via custom_context
Set custom_context on your LLMChain to propagate Langfuse trace attributes:
chain =
%{llm: llm, messages: messages}
|> LLMChain.new!()
|> Map.put(:custom_context, %{
langfuse_user_id: current_user.id,
langfuse_session_id: session_id,
langfuse_tags: ["production", "v2"],
langfuse_metadata: %{env: "prod", feature: "chat"}
})These are mapped to span attributes on the root chain span:
custom_context key | Span attribute |
|---|---|
: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.* |
Summary
Functions
Attaches OpenTelemetry handlers to LangChain telemetry events.
Detaches all OpenTelemetry handlers from LangChain telemetry events.
Executes a function with OpenTelemetry tracing suppressed.
Functions
@spec setup(keyword()) :: :ok
Attaches OpenTelemetry handlers to LangChain telemetry events.
Examples
LangChain.OpenTelemetry.setup()
LangChain.OpenTelemetry.setup(capture_input_messages: true, enable_metrics: false)
@spec teardown() :: :ok
Detaches all OpenTelemetry handlers from LangChain telemetry events.
@spec without_tracing((-> result)) :: result when result: any()
Executes a function with OpenTelemetry tracing suppressed.
Any LangChain operations inside the function will NOT create OTEL spans or traces, and — when metrics are enabled — will NOT emit duration/token metric events either. Useful for lightweight utility chains (translation, topic generation) that should not appear in your traces or skew your metrics.
Suppression is scoped to the calling process. Work that runs in a separate
process spawned inside the block (e.g. an async: true tool) does not inherit
it, the same way OTel span context does not cross the process boundary.
Example
LangChain.OpenTelemetry.without_tracing(fn ->
LLM.chain() |> LLMChain.run()
end)