Telemetry integration for Nous AI.
Nous executes the following Telemetry events:
Agent Events
[:nous, :agent, :run, :start]- Dispatched before agent execution starts- Measurement:
%{system_time: native_time, monotonic_time: monotonic_time} - Metadata:
%{agent_name: string, model_provider: atom, model_name: string, tool_count: integer, has_tools: boolean}
- Measurement:
[:nous, :agent, :run, :stop]- Dispatched after agent execution completes- Measurement:
%{duration: native_time, total_tokens: integer, input_tokens: integer, output_tokens: integer, tool_calls: integer, requests: integer, iterations: integer} - Metadata:
%{agent_name: string, model_provider: atom, model_name: string}
- Measurement:
[:nous, :agent, :run, :exception]- Dispatched when agent execution fails- Measurement:
%{duration: native_time} - Metadata:
%{agent_name: string, model_provider: atom, kind: atom, reason: term, stacktrace: list}
- Measurement:
[:nous, :agent, :iteration, :start]- Dispatched before each agent iteration- Measurement:
%{system_time: native_time} - Metadata:
%{agent_name: string, iteration: integer, max_iterations: integer}
- Measurement:
[:nous, :agent, :iteration, :stop]- Dispatched after each agent iteration- Measurement:
%{duration: native_time} - Metadata:
%{agent_name: string, iteration: integer, tool_calls: integer, needs_response: boolean}
- Measurement:
Provider Events
[:nous, :provider, :request, :start]- Dispatched before calling provider API- Measurement:
%{system_time: native_time, monotonic_time: monotonic_time} - Metadata:
%{provider: atom, model_name: string, message_count: integer}
- Measurement:
[:nous, :provider, :request, :stop]- Dispatched after provider responds- Measurement:
%{duration: native_time, input_tokens: integer, output_tokens: integer, total_tokens: integer} - Metadata:
%{provider: atom, model_name: string, has_tool_calls: boolean}
- Measurement:
[:nous, :provider, :request, :exception]- Dispatched when provider request fails- Measurement:
%{duration: native_time} - Metadata:
%{provider: atom, model_name: string, kind: atom, reason: term}
- Measurement:
Provider Streaming Events
[:nous, :provider, :stream, :start]- Dispatched before starting a streaming request- Measurement:
%{system_time: native_time, monotonic_time: monotonic_time} - Metadata:
%{provider: atom, model_name: string, message_count: integer}
- Measurement:
[:nous, :provider, :stream, :connected]- Dispatched when stream connection is established- Measurement:
%{duration: native_time} - Metadata:
%{provider: atom, model_name: string}
- Measurement:
[:nous, :provider, :stream, :exception]- Dispatched when streaming request fails- Measurement:
%{duration: native_time} - Metadata:
%{provider: atom, model_name: string, kind: atom, reason: term}
- Measurement:
Tool Events
[:nous, :tool, :execute, :start]- Dispatched before tool execution- Measurement:
%{system_time: native_time, monotonic_time: monotonic_time} Metadata:
%{tool_name: string, tool_module: module | nil, attempt: integer, max_retries: integer, has_timeout: boolean}
- Measurement:
[:nous, :tool, :execute, :stop]- Dispatched after tool completes- Measurement:
%{duration: native_time} - Metadata:
%{tool_name: string, attempt: integer, success: boolean}
- Measurement:
[:nous, :tool, :execute, :exception]- Dispatched when tool fails- Measurement:
%{duration: native_time} - Metadata:
%{tool_name: string, attempt: integer, will_retry: boolean, kind: atom, reason: term, stacktrace: list}
- Measurement:
[:nous, :tool, :timeout]- Dispatched when tool times out- Measurement:
%{timeout: integer} - Metadata:
%{tool_name: string}
- Measurement:
Context Events
[:nous, :context, :update]- Dispatched when context deps are updated by a tool- Measurement:
%{keys_updated: integer} - Metadata:
%{agent_name: string, keys: list(atom)}
- Measurement:
Callback Events
[:nous, :callback, :execute]- Dispatched when a callback is executed- Measurement:
%{duration: native_time} - Metadata:
%{callback_type: atom, agent_name: string}
- Measurement:
Fallback Events
[:nous, :agent, :fallback, :used]- Dispatched when an iteration switches to a fallback model (sticky-fallback). Use this to alert on provider degradation.- Measurement:
%{system_time: native_time} - Metadata:
%{agent_name, original_provider, original_model, active_provider, active_model}
- Measurement:
[:nous, :fallback, :activated]- Dispatched when the fallback chain promotes to the next model after the primary returns an error.- Measurement: depends on the call site
- Metadata: includes the active model and the reason for activation
Hook Events
[:nous, :hook, :execute, :start]/[:nous, :hook, :execute, :stop]Measurement:
%{system_time | duration}- Metadata:
%{event, hook_name, hook_type, result?}
[:nous, :hook, :denied]- emitted when a blocking hook returns :deny or when a fail_closed hook errors.- Metadata:
%{event, hook_name, hook_type, reason?}
- Metadata:
Skill Events
[:nous, :skill, :activate]/[:nous, :skill, :deactivate]- Metadata:
%{skill_name, agent_name}
- Metadata:
Workflow Events
[:nous, :workflow, :run, :start]/[:nous, :workflow, :run, :stop]/[:nous, :workflow, :run, :exception][:nous, :workflow, :node, :start]/[:nous, :workflow, :node, :stop]/[:nous, :workflow, :node, :exception]- See
Nous.Workflow.Telemetryfor full measurement/metadata payloads.
- See
All times are in :native time unit. Use System.convert_time_unit/3 to
convert to desired unit.
Default Handler
Nous provides a default handler that logs events at appropriate levels:
Nous.Telemetry.attach_default_handler()This is useful for development and debugging.
Custom Handlers
:telemetry.attach(
"my-nous-handler",
[:nous, :agent, :run, :stop],
fn _event, measurements, metadata, _config ->
MyApp.Metrics.track_agent_run(
metadata.agent_name,
measurements.duration,
measurements.total_tokens
)
end,
nil
)Metrics Integration
For production metrics, consider integrating with:
telemetry_metricsfor Prometheus/StatsDtelemetry_pollerfor periodic metricsPhoenix LiveDashboard for visualization
defmodule MyApp.Telemetry do
use Supervisor import Telemetry.Metrics def metrics do [ counter("nous.agent.run.start.count"), distribution("nous.agent.run.stop.duration", unit: {:native, :millisecond} ), sum("nous.agent.run.stop.total_tokens"), counter("nous.tool.execute.stop.count", tags: [:tool_name] ), counter("nous.tool.timeout.count", tags: [:tool_name] ) ] endend
Examples
Every event above is a plain :telemetry event, so attaching to one is just
:telemetry.attach/4. This handler forwards tool timeouts to the calling
process — the same shape you would use in a test to assert an event fired:
iex> :telemetry.attach(
...> "nous-doc-example",
...> [:nous, :tool, :timeout],
...> fn _event, measurements, metadata, pid ->
...> send(pid, {:tool_timeout, metadata.tool_name, measurements.timeout})
...> end,
...> self()
...> )
:ok
iex> :telemetry.execute([:nous, :tool, :timeout], %{timeout: 5000}, %{tool_name: "bash"})
:ok
iex> receive do
...> {:tool_timeout, tool_name, timeout} -> {tool_name, timeout}
...> after
...> 0 -> :no_event
...> end
{"bash", 5000}
iex> :telemetry.detach("nous-doc-example")
:okDurations are in :native units, so convert before reporting them:
iex> System.convert_time_unit(System.convert_time_unit(120, :millisecond, :native), :native, :millisecond)
120For local development, skip the wiring entirely and let Nous log everything it emits:
Nous.Telemetry.attach_default_handler()
{:ok, result} = Nous.run(agent, "Summarise this file")
# [Nous] Agent assistant completed in 1832ms (947 tokens, 1 tool calls, 2 iterations)
Nous.Telemetry.detach_default_handler()
Summary
Functions
@spec attach_default_handler() :: :ok | {:error, :already_exists}
Attaches the default logging handler for Nous events.
This handler logs:
- Agent runs (info level)
- Provider requests (debug level)
- Tool executions (debug level)
- Exceptions (error level)
Returns {:error, :already_exists} if it is already attached.
Example
Nous.Telemetry.attach_default_handler()
@spec detach_default_handler() :: :ok | {:error, :not_found}
Detaches the default handler.
Returns {:error, :not_found} if it was never attached.