Packages
langchain
0.9.2
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/open_telemetry/metrics_handler.ex
# Guarded on the `:opentelemetry` module from the `opentelemetry_api` optional
# dep (see `LangChain.OpenTelemetry` for the full rationale) — not the SDK app.
if Code.ensure_loaded?(:opentelemetry) do
defmodule LangChain.OpenTelemetry.MetricsHandler do
@moduledoc """
Telemetry handler that re-emits LangChain telemetry events as GenAI
Semantic Convention-aligned intermediary metric events.
**Important:** This module does not directly record OpenTelemetry histograms
or counters. It emits `:telemetry.execute/3` events that must be consumed by
a metrics library to become actual OTel metrics. Without a consumer attached
to these events, `enable_metrics: true` has no visible effect.
To produce actual OTel metrics, attach a consumer such as `Telemetry.Metrics`
with an OpenTelemetry reporter, `PromEx`, or equivalent to the events below.
## Emitted events
* `[:langchain, :otel, :operation, :duration]` — with `%{duration_s: float()}`
measurement and GenAI attributes as metadata. Emitted for both successful
(`:stop`) and failed (`:exception`) operations; failures additionally carry
an `error.type` attribute so error rate is observable alongside latency.
* `[:langchain, :otel, :token, :usage]` — with `%{tokens: integer()}` measurement
and GenAI attributes (including `gen_ai.token.type`) as metadata
* `[:langchain, :otel, :operation, :time_to_first_token]` — with
`%{duration_s: float()}` measurement and GenAI attributes as metadata.
Emitted once per streaming LLM call, measuring the time from request start to
the first streamed chunk (aligns with the semantic-convention
`gen_ai.server.time_to_first_token` metric).
## Usage
This module is used internally by `LangChain.OpenTelemetry.setup/1` when
`enable_metrics: true` (the default). You typically don't need to interact
with it directly.
"""
alias LangChain.OpenTelemetry.ProviderMapping
require Logger
@handler_prefix "langchain-otel-metrics"
@doc """
Returns the list of telemetry events this handler attaches to.
"""
@spec events() :: [list(atom())]
def events do
[
[:langchain, :llm, :call, :stop],
[:langchain, :llm, :call, :exception],
[:langchain, :llm, :stream, :first_token],
[:langchain, :chain, :execute, :stop],
[:langchain, :chain, :execute, :exception],
[:langchain, :tool, :call, :stop],
[:langchain, :tool, :call, :exception]
]
end
@doc """
Returns the telemetry handler ID prefix used for attaching/detaching.
"""
@spec handler_id() :: String.t()
def handler_id, do: @handler_prefix
@doc """
Telemetry handler callback. Re-emits duration and token usage metric events.
"""
@spec handle_event(list(atom()), map(), map(), term()) :: :ok
def handle_event(event, measurements, metadata, config) do
# Like `SpanHandler`: `:telemetry` permanently detaches a handler that
# raises (VM-wide, for the rest of the run). A single bad payload must never
# silently disable metrics for every subsequent request, so we trap and log.
#
# Skip operations running inside `without_tracing/1`: spans are dropped by
# the SDK's non-recording context, but this handler has no span context, so
# it needs the explicit flag to stay consistent (no metrics for utility
# chains either).
if LangChain.OpenTelemetry.telemetry_suppressed?() do
:ok
else
do_handle_event(event, measurements, metadata, config)
end
rescue
exception ->
Logger.warning(fn ->
"[LangChain.OpenTelemetry] metrics handler failed for #{inspect(event)} and was " <>
"skipped (metrics remain attached): " <>
Exception.format(:error, exception, __STACKTRACE__)
end)
:ok
end
defp do_handle_event(event, measurements, metadata, config)
defp do_handle_event(
[:langchain, :llm, :call, :stop],
measurements,
metadata,
_config
) do
common_attrs = common_attributes("chat", metadata)
emit_duration(measurements, common_attrs)
emit_token_usage(metadata, common_attrs)
:ok
end
defp do_handle_event(
[:langchain, :chain, :execute, :stop],
measurements,
metadata,
_config
) do
common_attrs = common_attributes("invoke_agent", metadata)
emit_duration(measurements, common_attrs)
:ok
end
defp do_handle_event(
[:langchain, :tool, :call, :stop],
measurements,
metadata,
_config
) do
common_attrs = common_attributes("execute_tool", metadata)
emit_duration(measurements, common_attrs)
:ok
end
defp do_handle_event(
[:langchain, :llm, :stream, :first_token],
measurements,
metadata,
_config
) do
case measurements[:duration] do
nil ->
:ok
duration_native ->
seconds =
System.convert_time_unit(duration_native, :native, :microsecond) / 1_000_000
:telemetry.execute(
[:langchain, :otel, :operation, :time_to_first_token],
%{duration_s: seconds},
common_attributes("chat", metadata)
)
end
:ok
end
# Failed operations: emit a duration metric tagged with `error.type` so error
# rate and error latency are observable alongside successes. No token usage is
# available on a failure.
defp do_handle_event(
[:langchain, component, operation, :exception],
measurements,
metadata,
_config
) do
operation_name = operation_name_for(component, operation)
attrs =
operation_name
|> common_attributes(metadata)
|> Map.put("error.type", error_type(metadata[:error]))
emit_duration(measurements, attrs)
:ok
end
defp operation_name_for(:llm, :call), do: "chat"
defp operation_name_for(:chain, :execute), do: "invoke_agent"
defp operation_name_for(:tool, :call), do: "execute_tool"
defp error_type(%module{}), do: inspect(module)
defp error_type(_), do: "error"
defp emit_duration(measurements, attrs) do
case measurements[:duration] do
nil ->
:ok
duration_native ->
duration_s =
System.convert_time_unit(duration_native, :native, :microsecond) / 1_000_000
:telemetry.execute(
[:langchain, :otel, :operation, :duration],
%{duration_s: duration_s},
attrs
)
end
end
defp emit_token_usage(metadata, common_attrs) do
case metadata[:token_usage] do
%{input: input, output: output} ->
if input do
attrs = Map.put(common_attrs, "gen_ai.token.type", "input")
:telemetry.execute(
[:langchain, :otel, :token, :usage],
%{tokens: input},
attrs
)
end
if output do
attrs = Map.put(common_attrs, "gen_ai.token.type", "output")
:telemetry.execute(
[:langchain, :otel, :token, :usage],
%{tokens: output},
attrs
)
end
:ok
_ ->
:ok
end
end
defp common_attributes(operation_name, metadata) do
attrs = %{"gen_ai.operation.name" => operation_name}
attrs =
case metadata[:provider] do
nil -> attrs
provider -> Map.put(attrs, "gen_ai.provider.name", ProviderMapping.to_otel(provider))
end
case metadata[:model] do
nil -> attrs
model -> Map.put(attrs, "gen_ai.request.model", model)
end
end
end
end