Skip to content

Commit 084a14d

Browse files
committed
Feat: Prometheus metrics integration for agent monitoring
1 parent c06cd45 commit 084a14d

6 files changed

Lines changed: 834 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Example: Prometheus metrics endpoint for agent monitoring.
2+
3+
This example shows how to set up a FastAPI server with a /metrics endpoint
4+
that exposes Prometheus metrics for your agents.
5+
6+
To run:
7+
pip install 'openai-agents[prometheus]' fastapi uvicorn
8+
uv run python examples/metrics/prometheus_endpoint.py
9+
10+
Then open http://localhost:8000/metrics in your browser or configure
11+
Prometheus to scrape http://localhost:8000/metrics
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import asyncio
17+
import time
18+
import random
19+
from contextlib import asynccontextmanager
20+
21+
from fastapi import FastAPI
22+
from prometheus_client import make_asgi_app
23+
24+
from agents import Agent, Runner
25+
from agents.metrics import PrometheusMetrics, MetricsHooks, enable_metrics
26+
27+
metrics = PrometheusMetrics()
28+
enable_metrics(metrics)
29+
30+
metrics_app = make_asgi_app()
31+
32+
agent = Agent(
33+
name="math_assistant",
34+
instructions="You are a helpful math assistant. Solve simple math problems.",
35+
)
36+
37+
38+
@asynccontextmanager
39+
async def lifespan(app: FastAPI):
40+
"""Lifespan context manager for startup/shutdown."""
41+
print("Starting server with metrics enabled...")
42+
print("Visit http://localhost:8000/metrics for Prometheus metrics")
43+
yield
44+
print("Shutting down...")
45+
46+
47+
app = FastAPI(title="Agent Metrics Example", lifespan=lifespan)
48+
49+
app.mount("/metrics", metrics_app)
50+
51+
52+
@app.get("/")
53+
async def root():
54+
"""Root endpoint with instructions."""
55+
return {
56+
"message": "Agent Metrics Example",
57+
"endpoints": {
58+
"/": "This help message",
59+
"/metrics": "Prometheus metrics endpoint",
60+
"/solve/{problem}": "Solve a math problem (generates metrics)",
61+
"/chat/{message}": "Chat with the agent (generates metrics)",
62+
},
63+
}
64+
65+
66+
@app.get("/solve/{problem}")
67+
async def solve(problem: str):
68+
"""Solve a math problem and record metrics."""
69+
hooks = MetricsHooks()
70+
71+
start_time = time.monotonic()
72+
73+
try:
74+
result = await Runner.run(
75+
agent,
76+
f"Solve this math problem: {problem}",
77+
hooks=[hooks],
78+
)
79+
80+
duration = time.monotonic() - start_time
81+
82+
return {
83+
"problem": problem,
84+
"solution": result.final_output,
85+
"duration_seconds": round(duration, 3),
86+
}
87+
except Exception as e:
88+
duration = time.monotonic() - start_time
89+
return {
90+
"problem": problem,
91+
"error": str(e),
92+
"duration_seconds": round(duration, 3),
93+
}
94+
95+
96+
@app.get("/chat/{message}")
97+
async def chat(message: str):
98+
"""Chat with the agent and record metrics."""
99+
hooks = MetricsHooks()
100+
101+
try:
102+
result = await Runner.run(
103+
agent,
104+
message,
105+
hooks=[hooks],
106+
)
107+
108+
return {
109+
"message": message,
110+
"response": result.final_output,
111+
"usage": {
112+
"input_tokens": result.usage.input_tokens if result.usage else 0,
113+
"output_tokens": result.usage.output_tokens if result.usage else 0,
114+
"total_tokens": result.usage.total_tokens if result.usage else 0,
115+
},
116+
}
117+
except Exception as e:
118+
return {
119+
"message": message,
120+
"error": str(e),
121+
}
122+
123+
124+
@app.post("/generate-load")
125+
async def generate_load(count: int = 10):
126+
"""Generate load for testing metrics (simulated)."""
127+
results = []
128+
129+
for i in range(count):
130+
operation = random.choice(["add", "multiply", "divide", "subtract"])
131+
a, b = random.randint(1, 100), random.randint(1, 100)
132+
133+
latency = random.uniform(0.1, 2.0)
134+
tokens_in = random.randint(50, 500)
135+
tokens_out = random.randint(20, 200)
136+
137+
metrics.record_llm_call(
138+
latency=latency,
139+
tokens_in=tokens_in,
140+
tokens_out=tokens_out,
141+
model="gpt-4",
142+
)
143+
144+
if random.random() < 0.1:
145+
error_type = random.choice(["RateLimitError", "TimeoutError", "APIError"])
146+
metrics.record_error(error_type, agent.name or "unknown")
147+
results.append(
148+
{
149+
"operation": operation,
150+
"error": error_type,
151+
}
152+
)
153+
else:
154+
results.append(
155+
{
156+
"operation": operation,
157+
"a": a,
158+
"b": b,
159+
"latency": round(latency, 3),
160+
}
161+
)
162+
163+
await asyncio.sleep(0.01)
164+
165+
return {
166+
"generated": count,
167+
"results": results,
168+
}
169+
170+
171+
if __name__ == "__main__":
172+
import uvicorn
173+
174+
print("""
175+
Endpoints:
176+
• http://localhost:8000/ - API documentation
177+
• http://localhost:8000/metrics - Prometheus metrics
178+
• http://localhost:8000/solve/{x} - Solve math problem
179+
• http://localhost:8000/chat/{msg} - Chat with agent
180+
• POST /generate-load?count=10 - Generate test load
181+
182+
Metrics available:
183+
• agents_llm_latency_seconds - LLM call latency
184+
• agents_tokens_total - Token usage
185+
• agents_errors_total - Error counts
186+
• agents_runs_total - Run counts
187+
• agents_run_duration_seconds - Run duration
188+
• agents_turns_total - LLM turns
189+
• agents_tool_executions_total - Tool executions
190+
• agents_tool_latency_seconds - Tool latency
191+
192+
""")
193+
194+
uvicorn.run(app, host="0.0.0.0", port=8000)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ sqlalchemy = ["SQLAlchemy>=2.0", "asyncpg>=0.29.0"]
4343
encrypt = ["cryptography>=45.0, <46"]
4444
redis = ["redis>=7"]
4545
dapr = ["dapr>=1.16.0", "grpcio>=1.60.0"]
46+
prometheus = ["prometheus-client>=0.21.0"]
4647

4748
[dependency-groups]
4849
dev = [

src/agents/metrics/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from .prometheus import PrometheusMetrics
8+
from .hooks import MetricsHooks
9+
10+
__all__ = [
11+
"PrometheusMetrics",
12+
"MetricsHooks",
13+
"enable_metrics",
14+
"get_metrics",
15+
"disable_metrics",
16+
]
17+
18+
19+
def __getattr__(name: str):
20+
if name == "PrometheusMetrics":
21+
from .prometheus import PrometheusMetrics as _PrometheusMetrics
22+
23+
return _PrometheusMetrics
24+
elif name == "MetricsHooks":
25+
from .hooks import MetricsHooks as _MetricsHooks
26+
27+
return _MetricsHooks
28+
elif name == "enable_metrics":
29+
from .hooks import enable_metrics as _enable_metrics
30+
31+
return _enable_metrics
32+
elif name == "get_metrics":
33+
from .hooks import get_metrics as _get_metrics
34+
35+
return _get_metrics
36+
elif name == "disable_metrics":
37+
from .hooks import disable_metrics as _disable_metrics
38+
39+
return _disable_metrics
40+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

src/agents/metrics/hooks.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
2+
from __future__ import annotations
3+
4+
import time
5+
from typing import Any
6+
7+
from ..agent import Agent
8+
from ..lifecycle import RunHooks
9+
from ..logger import logger
10+
from ..result import RunResult
11+
from ..run_context import RunContextWrapper
12+
13+
try:
14+
from .prometheus import PrometheusMetrics
15+
except ImportError:
16+
PrometheusMetrics = None
17+
18+
_global_metrics: PrometheusMetrics | None = None
19+
20+
21+
def enable_metrics(metrics: PrometheusMetrics) -> None:
22+
global _global_metrics
23+
_global_metrics = metrics
24+
25+
26+
def get_metrics() -> PrometheusMetrics | None:
27+
return _global_metrics
28+
29+
30+
def disable_metrics() -> None:
31+
global _global_metrics
32+
_global_metrics = None
33+
34+
35+
class MetricsHooks(RunHooks):
36+
37+
def __init__(self, metrics: PrometheusMetrics | None = None) -> None:
38+
self._metrics = metrics or _global_metrics
39+
self._run_start_times: dict[str, float] = {}
40+
self._tool_start_times: dict[str, float] = {}
41+
42+
async def on_start(
43+
self,
44+
context: RunContextWrapper[Any],
45+
agent: Agent[Any],
46+
) -> None:
47+
if self._metrics is None:
48+
return
49+
50+
agent_name = agent.name or "unknown"
51+
self._run_start_times[context.context_id] = time.monotonic()
52+
self._metrics.record_run_start(agent_name)
53+
54+
async def on_end(
55+
self,
56+
context: RunContextWrapper[Any],
57+
agent: Agent[Any],
58+
result: RunResult,
59+
) -> None:
60+
if self._metrics is None:
61+
return
62+
63+
agent_name = agent.name or "unknown"
64+
start_time = self._run_start_times.pop(context.context_id, None)
65+
duration = None
66+
if start_time is not None:
67+
duration = time.monotonic() - start_time
68+
69+
self._metrics.record_run_end(agent_name, duration, status="success")
70+
71+
async def on_error(
72+
self,
73+
context: RunContextWrapper[Any],
74+
agent: Agent[Any],
75+
error: Exception,
76+
) -> None:
77+
if self._metrics is None:
78+
return
79+
80+
agent_name = agent.name or "unknown"
81+
start_time = self._run_start_times.pop(context.context_id, None)
82+
duration = None
83+
if start_time is not None:
84+
duration = time.monotonic() - start_time
85+
86+
error_type = type(error).__name__
87+
self._metrics.record_error(error_type, agent_name)
88+
self._metrics.record_run_end(agent_name, duration, status="error")
89+
90+
async def on_tool_start(
91+
self,
92+
context: RunContextWrapper[Any],
93+
agent: Agent[Any],
94+
tool_name: str,
95+
input_data: dict[str, Any],
96+
) -> None:
97+
if self._metrics is None:
98+
return
99+
100+
key = f"{context.context_id}:{tool_name}"
101+
self._tool_start_times[key] = time.monotonic()
102+
103+
async def on_tool_end(
104+
self,
105+
context: RunContextWrapper[Any],
106+
agent: Agent[Any],
107+
tool_name: str,
108+
result: Any,
109+
) -> None:
110+
if self._metrics is None:
111+
return
112+
113+
key = f"{context.context_id}:{tool_name}"
114+
start_time = self._tool_start_times.pop(key, None)
115+
if start_time is not None:
116+
latency = time.monotonic() - start_time
117+
agent_name = agent.name or "unknown"
118+
self._metrics.record_tool_execution(tool_name, latency, agent_name)
119+
120+
async def on_tool_error(
121+
self,
122+
context: RunContextWrapper[Any],
123+
agent: Agent[Any],
124+
tool_name: str,
125+
error: Exception,
126+
) -> None:
127+
if self._metrics is None:
128+
return
129+
130+
key = f"{context.context_id}:{tool_name}"
131+
start_time = self._tool_start_times.pop(key, None)
132+
if start_time is not None:
133+
latency = time.monotonic() - start_time
134+
agent_name = agent.name or "unknown"
135+
self._metrics.record_tool_execution(tool_name, latency, agent_name)
136+
137+
error_type = f"tool_error:{type(error).__name__}"
138+
agent_name = agent.name or "unknown"
139+
self._metrics.record_error(error_type, agent_name)
140+
141+
142+
def create_metrics_hooks(metrics: PrometheusMetrics | None = None) -> MetricsHooks:
143+
return MetricsHooks(metrics)

0 commit comments

Comments
 (0)