Packages

An Elixir library for LLM agent observability. Provides instrumentation for agent loops, tool calls, and LLM requests with support for OpenTelemetry and OpenInference semantic conventions.

Current section

Files

Jump to
agent_obs lib agent_obs handler.ex
Raw

lib/agent_obs/handler.ex

defmodule AgentObs.Handler do
@moduledoc """
Behaviour for AgentObs backend handlers.
Handlers receive telemetry events emitted by AgentObs instrumentation
and translate them to backend-specific formats (OpenTelemetry spans,
logs, metrics, etc.).
## Implementing a Handler
To create a custom handler, implement this behaviour in a GenServer:
defmodule MyApp.CustomHandler do
use GenServer
@behaviour AgentObs.Handler
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl AgentObs.Handler
def attach(config) do
event_prefix = Map.get(config, :event_prefix, [:agent_obs])
handler_id = {:my_custom_handler, event_prefix, self()}
events_to_attach = [
event_prefix ++ [:agent, :start],
event_prefix ++ [:agent, :stop],
# ... more events
]
:ok = :telemetry.attach_many(
handler_id,
events_to_attach,
&__MODULE__.handle_event/4,
config
)
{:ok, %{handler_id: handler_id, config: config}}
end
@impl AgentObs.Handler
def handle_event(event_name, measurements, metadata, config) do
# Process the event
IO.inspect({event_name, measurements, metadata})
:ok
end
@impl AgentObs.Handler
def detach(state) do
:telemetry.detach(state.handler_id)
end
# GenServer callbacks
@impl GenServer
def init(opts) do
case attach(opts) do
{:ok, state} -> {:ok, state}
{:error, reason} -> {:stop, reason}
end
end
@impl GenServer
def terminate(_reason, state) do
detach(state)
end
end
## Configuration
Handlers are configured in the application config:
config :agent_obs,
handlers: [MyApp.CustomHandler]
config :agent_obs, MyApp.CustomHandler,
custom_option: "value"
The handler will receive this configuration in the `attach/1` callback.
## Synchronous Execution
Handler callbacks are executed synchronously when telemetry events are emitted.
Keep processing fast to avoid blocking the application. For expensive operations,
consider sending work to a separate process or using asynchronous export.
"""
@doc """
Attaches the handler to telemetry events.
Called during handler initialization. Should use `:telemetry.attach_many/4`
to register for relevant events.
Returns `{:ok, state}` or `{:error, reason}`.
## Parameters
- `config` - Configuration map for the handler, typically read from application config
## Examples
def attach(config) do
event_prefix = Map.get(config, :event_prefix, [:agent_obs])
handler_id = {:my_handler, event_prefix, self()}
:ok = :telemetry.attach_many(
handler_id,
[event_prefix ++ [:agent, :start]],
&__MODULE__.handle_event/4,
config
)
{:ok, %{handler_id: handler_id}}
end
"""
@callback attach(config :: map()) :: {:ok, term()} | {:error, term()}
@doc """
Handles a telemetry event.
Called synchronously when an attached event is emitted. Should process
the event quickly to avoid blocking the emitting process.
## Parameters
- `event_name` - The full event name as a list of atoms (e.g., `[:agent_obs, :llm, :start]`)
- `measurements` - Map of measurements (e.g., `%{duration: 1000000}` for :stop events)
- `metadata` - Event-specific metadata according to AgentObs.Events schema
- `config` - The handler configuration passed during attachment
## Examples
def handle_event([:agent_obs, :llm, :start], _measurements, metadata, _config) do
IO.inspect(metadata.model)
:ok
end
"""
@callback handle_event(
event_name :: [atom()],
measurements :: map(),
metadata :: map(),
config :: term()
) :: :ok
@doc """
Detaches the handler from telemetry events.
Called during handler termination. Should clean up any resources
and detach from telemetry events.
## Parameters
- `state` - The handler's internal state
## Examples
def detach(state) do
:telemetry.detach(state.handler_id)
end
"""
@callback detach(state :: term()) :: :ok
end