Packages
nous
0.16.6
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/telemetry.ex
defmodule Nous.Telemetry do
@moduledoc """
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}`
* `[: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}`
* `[: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}`
* `[:nous, :agent, :iteration, :start]` - Dispatched before each agent iteration
* Measurement: `%{system_time: native_time}`
* Metadata: `%{agent_name: string, iteration: integer, max_iterations: integer}`
* `[: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}`
## 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}`
* `[: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}`
* `[:nous, :provider, :request, :exception]` - Dispatched when provider request fails
* Measurement: `%{duration: native_time}`
* Metadata: `%{provider: atom, model_name: string, kind: atom, reason: term}`
## 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}`
* `[:nous, :provider, :stream, :connected]` - Dispatched when stream connection is established
* Measurement: `%{duration: native_time}`
* Metadata: `%{provider: atom, model_name: string}`
* `[:nous, :provider, :stream, :exception]` - Dispatched when streaming request fails
* Measurement: `%{duration: native_time}`
* Metadata: `%{provider: atom, model_name: string, kind: atom, reason: term}`
## 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}`
* `[:nous, :tool, :execute, :stop]` - Dispatched after tool completes
* Measurement: `%{duration: native_time}`
* Metadata: `%{tool_name: string, attempt: integer, success: boolean}`
* `[: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}`
* `[:nous, :tool, :timeout]` - Dispatched when tool times out
* Measurement: `%{timeout: integer}`
* Metadata: `%{tool_name: string}`
## 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)}`
## Callback Events
* `[:nous, :callback, :execute]` - Dispatched when a callback is executed
* Measurement: `%{duration: native_time}`
* Metadata: `%{callback_type: atom, agent_name: string}`
## 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}`
* `[: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?}`
## Skill Events
* `[:nous, :skill, :activate]` / `[:nous, :skill, :deactivate]`
* Metadata: `%{skill_name, agent_name}`
## 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.Telemetry` for full measurement/metadata payloads.
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_metrics` for Prometheus/StatsD
- `telemetry_poller` for periodic metrics
- Phoenix 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]
)
]
end
end
"""
require Logger
@doc """
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)
## Example
Nous.Telemetry.attach_default_handler()
"""
def attach_default_handler do
events = [
# Agent events
[:nous, :agent, :run, :start],
[:nous, :agent, :run, :stop],
[:nous, :agent, :run, :exception],
[:nous, :agent, :iteration, :start],
[:nous, :agent, :iteration, :stop],
# Provider events
[:nous, :provider, :request, :start],
[:nous, :provider, :request, :stop],
[:nous, :provider, :request, :exception],
[:nous, :provider, :stream, :start],
[:nous, :provider, :stream, :connected],
[:nous, :provider, :stream, :exception],
# Tool events
[:nous, :tool, :execute, :start],
[:nous, :tool, :execute, :stop],
[:nous, :tool, :execute, :exception],
[:nous, :tool, :timeout],
# Context events
[:nous, :context, :update],
# Callback events
[:nous, :callback, :execute]
]
:telemetry.attach_many(
"nous-default-handler",
events,
&handle_event/4,
nil
)
end
@doc """
Detaches the default handler.
"""
def detach_default_handler do
:telemetry.detach("nous-default-handler")
end
# Event handlers
defp handle_event([:nous, :agent, :run, :start], _measurements, metadata, _config) do
Logger.info(
"[Nous] Agent #{metadata.agent_name} starting (#{metadata.model_provider}:#{metadata.model_name})"
)
end
defp handle_event([:nous, :agent, :run, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info(
"[Nous] Agent #{metadata.agent_name} completed in #{duration_ms}ms " <>
"(#{measurements.total_tokens} tokens, #{measurements.tool_calls} tool calls, #{measurements.iterations} iterations)"
)
end
defp handle_event([:nous, :agent, :run, :exception], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.error(
"[Nous] Agent #{metadata.agent_name} failed after #{duration_ms}ms: " <>
"#{inspect(metadata.reason)}"
)
end
defp handle_event([:nous, :provider, :request, :start], _measurements, metadata, _config) do
Logger.debug("[Nous] Provider request to #{metadata.provider}:#{metadata.model_name}")
end
defp handle_event([:nous, :provider, :request, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug(
"[Nous] Provider #{metadata.provider}:#{metadata.model_name} responded in #{duration_ms}ms " <>
"(#{measurements.total_tokens} tokens)"
)
end
defp handle_event([:nous, :provider, :request, :exception], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.error(
"[Nous] Provider #{metadata.provider}:#{metadata.model_name} failed after #{duration_ms}ms: " <>
summarize_reason(metadata.reason)
)
end
defp handle_event([:nous, :provider, :stream, :start], _measurements, metadata, _config) do
Logger.debug("[Nous] Stream request to #{metadata.provider}:#{metadata.model_name}")
end
defp handle_event([:nous, :provider, :stream, :connected], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug(
"[Nous] Stream #{metadata.provider}:#{metadata.model_name} connected in #{duration_ms}ms"
)
end
defp handle_event([:nous, :provider, :stream, :exception], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.error(
"[Nous] Stream #{metadata.provider}:#{metadata.model_name} failed after #{duration_ms}ms: " <>
summarize_reason(metadata.reason)
)
end
defp handle_event([:nous, :tool, :execute, :start], _measurements, metadata, _config) do
Logger.debug("[Nous] Tool #{metadata.tool_name} executing (attempt #{metadata.attempt})")
end
defp handle_event([:nous, :tool, :execute, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug(
"[Nous] Tool #{metadata.tool_name} #{if metadata.success, do: "succeeded", else: "failed"} " <>
"in #{duration_ms}ms (attempt #{metadata.attempt})"
)
end
defp handle_event([:nous, :tool, :execute, :exception], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
retry_msg = if metadata.will_retry, do: " (will retry)", else: " (final attempt)"
Logger.warning(
"[Nous] Tool #{metadata.tool_name} failed after #{duration_ms}ms#{retry_msg}: " <>
"#{inspect(metadata.reason)}"
)
end
# Tool timeout
defp handle_event([:nous, :tool, :timeout], measurements, metadata, _config) do
Logger.warning("[Nous] Tool #{metadata.tool_name} timed out after #{measurements.timeout}ms")
end
# Agent iteration events
defp handle_event([:nous, :agent, :iteration, :start], _measurements, metadata, _config) do
Logger.debug(
"[Nous] Agent #{metadata.agent_name} iteration #{metadata.iteration}/#{metadata.max_iterations}"
)
end
defp handle_event([:nous, :agent, :iteration, :stop], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
continue_msg = if metadata.needs_response, do: " (continuing)", else: " (done)"
Logger.debug(
"[Nous] Agent #{metadata.agent_name} iteration #{metadata.iteration} completed in #{duration_ms}ms" <>
" (#{metadata.tool_calls} tool calls)#{continue_msg}"
)
end
# Context update events
defp handle_event([:nous, :context, :update], measurements, metadata, _config) do
Logger.debug(
"[Nous] Context updated for #{metadata.agent_name}: #{measurements.keys_updated} keys (#{inspect(metadata.keys)})"
)
end
# Callback events
defp handle_event([:nous, :callback, :execute], measurements, metadata, _config) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug(
"[Nous] Callback #{metadata.callback_type} executed for #{metadata.agent_name} in #{duration_ms}ms"
)
end
# Ignore unknown events for forward compatibility
defp handle_event(_event, _measurements, _metadata, _config) do
:ok
end
# Reduce a provider error to a bounded, header/body-light summary. The raw
# term is an HTTP error map (%{status, body, headers}) whose body can be large
# and echo provider-side context; logging it verbatim is a log-volume and
# reconnaissance risk. Drop headers, cap the body snippet.
defp summarize_reason(%{status: status} = err) do
"status=#{status}#{body_snippet(Map.get(err, :body))}"
end
defp summarize_reason(reason) when is_exception(reason), do: Exception.message(reason)
defp summarize_reason(reason) do
reason |> inspect(limit: 5, printable_limit: 200) |> String.slice(0, 300)
end
defp body_snippet(body) when is_binary(body), do: " body=#{String.slice(body, 0, 200)}"
defp body_snippet(_), do: ""
end