ReqLLM.OpenTelemetry.Adapter behaviour (ReqLLM v1.12.0)

View Source

Behaviour the OpenTelemetry bridge uses to talk to a tracer.

ReqLLM.OpenTelemetry ships ReqLLM.OpenTelemetry.OTelAdapter as the default implementation, which calls the standard :otel_tracer and :otel_span API. Implement this behaviour to swap in a different tracer, inject extra attributes on every span (e.g. caller-context like langfuse.user.id), or run the bridge in test mode without an OpenTelemetry SDK.

Pass your module via :adapter:

ReqLLM.OpenTelemetry.attach("req-llm-otel", adapter: MyApp.ReqLLMAdapter)

Required callbacks

available?/0, start_span/3, set_attributes/3, add_event/4, set_status/4, end_span/2.

Optional callbacks (metrics)

metrics_available?/0, record_histogram/2. The bridge only invokes these when both available?/0 and metrics_available?/0 return true.

Optional callbacks (child spans for server-side tool execution)

start_child_span/5, end_span_at/3. The bridge invokes these to emit gen_ai.execute_tool child spans for server-side builtin tool calls (e.g. web_search_call on the OpenAI Responses API). Adapters that don't implement them get a fallback: the bridge calls start_span/3 + end_span/2 instead, which records the same data but loses the parent-child relationship (the sub-spans appear as siblings).

Example — inject caller-context on every ReqLLM span

The cleanest way to wrap the default adapter is to delegate everything and override just start_span/3 to merge in extra attributes:

defmodule MyApp.ReqLLMAdapter do
  @behaviour ReqLLM.OpenTelemetry.Adapter

  defdelegate available?(), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate metrics_available?(), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate set_attributes(s, a, c), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate add_event(s, n, a, c), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate set_status(s, k, m, c), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate end_span(s, c), to: ReqLLM.OpenTelemetry.OTelAdapter
  defdelegate record_histogram(r, c), to: ReqLLM.OpenTelemetry.OTelAdapter

  def start_span(name, attrs, config) do
    extras = %{"langfuse.user.id" => Process.get(:current_user_id)}
    ReqLLM.OpenTelemetry.OTelAdapter.start_span(name, Map.merge(attrs, extras), config)
  end
end

ReqLLM.OpenTelemetry.attach("req-llm-otel", adapter: MyApp.ReqLLMAdapter)

See the Telemetry guide's caller-context section for when to use this versus a parent span or OTel baggage.

Summary

Callbacks

add_event(term, arg2, map, keyword)

@callback add_event(term(), atom() | String.t(), map(), keyword()) :: :ok

available?()

@callback available?() :: boolean()

end_span(term, keyword)

@callback end_span(
  term(),
  keyword()
) :: :ok

end_span_at(span, end_time, config)

(optional)
@callback end_span_at(span :: term(), end_time :: integer(), config :: keyword()) :: :ok

metrics_available?()

(optional)
@callback metrics_available?() :: boolean()

record_histogram(map, keyword)

(optional)
@callback record_histogram(
  map(),
  keyword()
) :: :ok

set_attributes(term, map, keyword)

@callback set_attributes(term(), map(), keyword()) :: :ok

set_status(term, arg2, arg3, keyword)

@callback set_status(term(), :ok | :error, String.t() | nil, keyword()) :: :ok

start_child_span(parent, name, attributes, opts, config)

(optional)
@callback start_child_span(
  parent :: term(),
  name :: String.t(),
  attributes :: map(),
  opts :: %{optional(:kind) => atom(), optional(:start_time) => integer()},
  config :: keyword()
) :: term()

start_span(t, map, keyword)

@callback start_span(String.t(), map(), keyword()) :: term()