Skip to main content

lib/agent_sea/telemetry.ex

defmodule AgentSea.Telemetry do
  @moduledoc """
  Telemetry events emitted across AgentSea. Attach handlers — a Logger, a
  Prometheus exporter, or a Phoenix LiveView dashboard — to observe agent,
  provider, tool, and crew activity without any bespoke event bus.

  All events are under the `:agentsea` prefix.

  Span events (emit `:start`, `:stop`, and `:exception` via `:telemetry.span/3`):

    * `[:agentsea, :agent, :run, _]` — a full `AgentSea.Agent.run/3`
      * metadata: `%{name, model}`; `:stop` adds `%{outcome, stop_reason}`
    * `[:agentsea, :provider, :complete, _]` — one provider completion in the loop
      * metadata: `%{provider, model, name, iteration}`;
        `:stop` adds `%{outcome, stop_reason, input_tokens, output_tokens}`
    * `[:agentsea, :tool, :run, _]` — one tool execution
      * metadata: `%{tool, agent}`; `:stop` adds `%{outcome}`

  Discrete events (emit `:start` and `:stop` via `:telemetry.execute/3`):

    * `[:agentsea, :crew, :kickoff, :start | :stop]` — a crew run
      * metadata: `%{crew, task_count}`; `:stop` adds `%{success}`,
        measurements `%{duration}`
    * `[:agentsea, :crew, :task, :start | :stop]` — one crew task
      * metadata: `%{crew, task_id, agent}`; `:stop` carries `%{crew, task_id, outcome}`
    * `[:agentsea, :gateway, :route, :stop]` — a gateway routing decision
      * metadata: `%{provider, outcome}`; measurements `%{attempts, latency_ms}`
    * `[:agentsea, :guardrail, :stop]` — a guardrail acted on content (emitted
      only when a guardrail transforms or blocks, not on a plain pass)
      * metadata: `%{guardrail, outcome}` where `outcome` is `:transform | :block`
  """

  require Logger

  @spans [
    [:agentsea, :agent, :run],
    [:agentsea, :provider, :complete],
    [:agentsea, :tool, :run]
  ]

  @discrete [
    [:agentsea, :crew, :kickoff],
    [:agentsea, :crew, :task],
    [:agentsea, :gateway, :route],
    [:agentsea, :guardrail]
  ]

  @doc "Every event name AgentSea may emit."
  @spec events() :: [[atom()]]
  def events do
    span_events = Enum.flat_map(@spans, &expand(&1, [:start, :stop, :exception]))
    discrete_events = Enum.flat_map(@discrete, &expand(&1, [:start, :stop]))
    span_events ++ discrete_events
  end

  defp expand(base, suffixes), do: Enum.map(suffixes, &(base ++ [&1]))

  @doc "Attach a Logger handler for all AgentSea events (handy in dev)."
  @spec attach_default_logger(Logger.level()) :: :ok | {:error, :already_exists}
  def attach_default_logger(level \\ :debug) do
    :telemetry.attach_many(
      "agentsea-default-logger",
      events(),
      &__MODULE__.handle_event/4,
      %{level: level}
    )
  end

  @doc false
  def handle_event(event, measurements, metadata, %{level: level}) do
    Logger.log(level, fn ->
      "#{Enum.join(event, ".")} #{inspect(measurements)} #{inspect(metadata)}"
    end)
  end
end