Skip to content

Observability

TelemetryHooks instruments every agent run with OpenTelemetry spans. Configure any OTLP-compatible backend — Jaeger, Honeycomb, Grafana Tempo, Datadog, or the console — then pass TelemetryHooks() to AgentConfig.

from cyclops import Agent, AgentConfig, TelemetryHooks
agent = Agent(AgentConfig(model="groq/llama-3.1-8b-instant", hooks=TelemetryHooks.console()))
agent.run("Summarise the Pythagorean theorem.")

TelemetryHooks.console() sets up a TracerProvider and ConsoleSpanExporter automatically — no boilerplate required. Spans are flushed automatically at the end of each run.

Each agent.run() call produces a root span with nested children:

agent.run
├── llm.completion
├── tool.<name>
├── llm.completion
└── tool.<name>
SpanAttributeDescription
agent.runagent.input.lengthCharacter length of the input message
agent.runagent.output.lengthCharacter length of the final response
llm.completionllm.message_countNumber of messages sent to the LLM
llm.completionllm.modelModel identifier returned by the API
llm.completionllm.prompt_tokensPrompt token count
llm.completionllm.completion_tokensCompletion token count
llm.completionllm.total_tokensTotal token count
llm.completionllm.latency_msRound-trip latency in milliseconds
tool.<name>tool.nameTool name
tool.<name>tool.args_countNumber of arguments passed
tool.<name>tool.result.lengthCharacter length of the tool result
Anyerrortrue when the span represents a failure
Anyerror.messageException message
Anyerror.typeException class name
from cyclops import Agent, AgentConfig, TelemetryHooks
hooks = TelemetryHooks.otlp("http://localhost:4317")
agent = Agent(AgentConfig(model="groq/llama-3.1-8b-instant", hooks=hooks))
agent.run("Summarise the Pythagorean theorem.")
hooks.flush()

Install the OTLP exporter first:

Terminal window
uv add opentelemetry-exporter-otlp-proto-grpc

Start Jaeger locally:

Terminal window
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest

Then open http://localhost:16686 and search for service cyclops.

on_run_end is not called for stream() / astream(), so the root agent.run span is not closed for streaming runs. Use run() or arun() if you need complete traces.

examples/otel_example.py
"""OpenTelemetry observability example.
Demonstrates wiring TelemetryHooks to an agent and emitting spans to the console.
Use TelemetryHooks.otlp("http://localhost:4317") to send to Jaeger, Honeycomb,
Grafana Tempo, Datadog, or any OTLP-compatible backend.
Span hierarchy per agent.run():
agent.run
├── llm.completion (attributes: model, tokens, latency_ms)
├── tool.<name> (attributes: name, args_count, result.length)
└── ...
"""
from cyclops import Agent, AgentConfig, TelemetryHooks
from cyclops.toolkit import tool
MODEL = "ollama/qwen3:4b"
# Alternatives:
# MODEL = "groq/llama-3.1-8b-instant" # GROQ_API_KEY
# MODEL = "anthropic/claude-haiku-4-5-20251001" # ANTHROPIC_API_KEY
# MODEL = "gpt-4o-mini" # OPENAI_API_KEY
# ---------------------------------------------------------------------------
# 2. Tools
# ---------------------------------------------------------------------------
@tool
def get_weather(location: str) -> str:
"""Get the current weather for a location."""
return f"Sunny, 22C in {location}"
@tool
def calculate(expression: str) -> str:
"""Evaluate a simple arithmetic expression."""
try:
return str(eval(expression)) # noqa: S307 (demo only)
except Exception as exc:
return f"Error: {exc}"
# ---------------------------------------------------------------------------
# 3. Run agent with TelemetryHooks
# ---------------------------------------------------------------------------
def main() -> None:
# TelemetryHooks.console() sets up a TracerProvider + ConsoleSpanExporter in one call.
# Swap for TelemetryHooks.otlp("http://localhost:4317") to send to Jaeger/Tempo/etc.
hooks = TelemetryHooks.console()
agent = Agent(
AgentConfig(model=MODEL, hooks=hooks),
tools=[get_weather, calculate],
)
print("Running agent...\n")
agent.run("What is the weather in Tokyo, and what is 42 multiplied by 17?")
print("\nDone. Spans printed above.")
if __name__ == "__main__":
main()