Skip to content

Commit b53e884

Browse files
committed
feat: Use instrumentation setup from framework and add metrics
1 parent 1f658b8 commit b53e884

4 files changed

Lines changed: 207 additions & 5 deletions

File tree

msaf/README.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ This package provides utilities to convert a [Microsoft Agent Framework](https:/
88

99
```python
1010
from agent_framework import Agent
11+
from agenticlayer.msaf import create_metrics_middleware, create_openai_client
1112
from agenticlayer.msaf.agent_to_a2a import to_a2a
12-
from agenticlayer.msaf.client import create_openai_client
1313

1414
agent = Agent(
1515
client=create_openai_client(),
1616
name="MyAgent",
1717
instructions="You are a helpful assistant.",
18+
middleware=create_metrics_middleware(),
1819
)
1920
app = to_a2a(agent, name="MyAgent", rpc_url="http://localhost:8000/")
2021
# Then run with: uvicorn module:app
@@ -35,3 +36,62 @@ such as [LiteLLM proxy](https://docs.litellm.ai/docs/proxy/quick_start):
3536

3637
`create_openai_client()` reads these variables automatically and passes them to
3738
`OpenAIChatClient` as `base_url` and `api_key`.
39+
40+
## Observability
41+
42+
### OpenTelemetry setup
43+
44+
Call `setup_otel()` before creating agents to configure OTLP exporters and enable instrumentation:
45+
46+
```python
47+
from agenticlayer.msaf.otel import setup_otel
48+
49+
setup_otel()
50+
```
51+
52+
This reads standard `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_PROTOCOL` environment
53+
variables, sets up trace/log/metric providers, and enables the built-in Agent Framework telemetry
54+
layers.
55+
56+
### Metrics
57+
58+
The SDK emits the following OpenTelemetry metrics:
59+
60+
**Built-in** (provided by Agent Framework telemetry layers, enabled by `setup_otel()`):
61+
62+
| Metric | Type | Description |
63+
|---|---|---|
64+
| `gen_ai.client.token.usage` | Histogram | Input and output token counts per LLM call |
65+
| `gen_ai.client.operation.duration` | Histogram | Duration of LLM / agent operations |
66+
67+
**Custom** (provided by `create_metrics_middleware()`, must be added to the agent):
68+
69+
| Metric | Type | Description |
70+
|---|---|---|
71+
| `agent.invocations` | Counter | Number of agent invocations |
72+
| `agent.llm.calls` | Counter | Number of LLM calls |
73+
| `agent.tool.calls` | Counter | Number of tool calls |
74+
| `agent.errors` | Counter | Number of errors (with `error_source` attribute) |
75+
76+
Add the metrics middleware to your agent:
77+
78+
```python
79+
from agent_framework import Agent
80+
from agenticlayer.msaf import create_metrics_middleware, create_openai_client
81+
82+
agent = Agent(
83+
client=create_openai_client(),
84+
instructions="You are a helpful assistant.",
85+
middleware=create_metrics_middleware(),
86+
)
87+
```
88+
89+
If you already have other middleware, combine them:
90+
91+
```python
92+
agent = Agent(
93+
client=create_openai_client(),
94+
instructions="You are a helpful assistant.",
95+
middleware=[MyCustomMiddleware(), *create_metrics_middleware()],
96+
)
97+
```

msaf/agenticlayer/msaf/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"""
66

77
from agenticlayer.msaf.client import create_openai_client
8+
from agenticlayer.msaf.metrics_middleware import create_metrics_middleware
89

9-
__all__ = ["create_openai_client"]
10+
__all__ = ["create_openai_client", "create_metrics_middleware"]
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Middleware that records agent metrics using OpenTelemetry.
3+
Tracks agent invocations, LLM calls, tool calls, and errors.
4+
5+
Token usage and operation duration are already provided by the built-in
6+
``agent_framework.observability`` telemetry layers and do not need to be
7+
duplicated here.
8+
"""
9+
10+
from collections.abc import Awaitable, Callable
11+
from typing import Any
12+
13+
from agent_framework import (
14+
AgentContext,
15+
AgentMiddleware,
16+
ChatContext,
17+
ChatMiddleware,
18+
FunctionInvocationContext,
19+
FunctionMiddleware,
20+
MiddlewareTypes,
21+
)
22+
from opentelemetry import metrics
23+
24+
_meter = metrics.get_meter("agenticlayer.agent")
25+
26+
_agent_invocations = _meter.create_counter(
27+
"agent.invocations",
28+
unit="{invocation}",
29+
description="Number of agent invocations",
30+
)
31+
_llm_calls = _meter.create_counter(
32+
"agent.llm.calls",
33+
unit="{call}",
34+
description="Number of LLM calls",
35+
)
36+
_tool_calls = _meter.create_counter(
37+
"agent.tool.calls",
38+
unit="{call}",
39+
description="Number of tool calls",
40+
)
41+
_agent_errors = _meter.create_counter(
42+
"agent.errors",
43+
unit="{error}",
44+
description="Number of agent errors",
45+
)
46+
47+
48+
class AgentInvocationMetrics(AgentMiddleware):
49+
"""Counts agent invocations and errors."""
50+
51+
async def process(
52+
self,
53+
context: AgentContext,
54+
call_next: Callable[[], Awaitable[None]],
55+
) -> None:
56+
agent_name = getattr(context.agent, "name", None) or "unknown"
57+
_agent_invocations.add(1, {"agent_name": agent_name})
58+
try:
59+
await call_next()
60+
except Exception:
61+
_agent_errors.add(1, {"agent_name": agent_name, "error_source": "agent"})
62+
raise
63+
64+
65+
class LlmCallMetrics(ChatMiddleware):
66+
"""Counts LLM / chat-client calls and records model-level errors."""
67+
68+
async def process(
69+
self,
70+
context: ChatContext,
71+
call_next: Callable[[], Awaitable[None]],
72+
) -> None:
73+
options = context.options or {}
74+
model: str = options.get("model_id") or getattr(context.client, "model_id", None) or "unknown"
75+
attrs: dict[str, Any] = {"model": model}
76+
_llm_calls.add(1, attrs)
77+
try:
78+
await call_next()
79+
except Exception:
80+
_agent_errors.add(1, {**attrs, "error_source": "model"})
81+
raise
82+
83+
84+
class ToolCallMetrics(FunctionMiddleware):
85+
"""Counts tool / function calls and records tool-level errors."""
86+
87+
async def process(
88+
self,
89+
context: FunctionInvocationContext,
90+
call_next: Callable[[], Awaitable[None]],
91+
) -> None:
92+
tool_name = getattr(context.function, "name", None) or "unknown"
93+
_tool_calls.add(1, {"tool_name": tool_name})
94+
try:
95+
await call_next()
96+
except Exception:
97+
_agent_errors.add(1, {"tool_name": tool_name, "error_source": "tool"})
98+
raise
99+
100+
101+
def create_metrics_middleware() -> list[MiddlewareTypes]:
102+
"""Return the full set of metrics middleware ready to pass to an Agent.
103+
104+
Example::
105+
106+
from agent_framework import Agent
107+
from agenticlayer.msaf.metrics_middleware import create_metrics_middleware
108+
109+
agent = Agent(
110+
client=client,
111+
middleware=create_metrics_middleware(),
112+
)
113+
"""
114+
return [AgentInvocationMetrics(), LlmCallMetrics(), ToolCallMetrics()]

msaf/agenticlayer/msaf/otel.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,37 @@
11
"""OpenTelemetry setup for a Microsoft Agent Framework Agent App."""
22

3-
from agenticlayer.shared.otel import setup_otel as _setup_otel_shared
3+
import logging
4+
5+
from agent_framework.observability import configure_otel_providers
6+
from agenticlayer.shared.otel import request_hook, response_hook
7+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
48

59
__all__ = ["setup_otel"]
610

11+
_logger = logging.getLogger(__name__)
12+
713

814
def setup_otel() -> None:
9-
"""Set up OpenTelemetry tracing, logging and metrics for a Microsoft Agent Framework agent."""
10-
_setup_otel_shared()
15+
"""Set up OpenTelemetry tracing, logging and metrics for a Microsoft Agent Framework agent.
16+
17+
Uses the built-in ``agent_framework`` OTLP provider setup (reads standard
18+
``OTEL_EXPORTER_OTLP_*`` environment variables) and enables the telemetry
19+
layers that emit ``gen_ai.client.token.usage`` and
20+
``gen_ai.client.operation.duration`` metrics.
21+
22+
Additionally instruments HTTPX clients so outgoing HTTP calls (to
23+
sub-agents, MCP servers, LLM gateways) are traced with debug-level
24+
request/response body logging.
25+
26+
Starlette server instrumentation is handled separately by
27+
:func:`agenticlayer.shared.otel_starlette.instrument_starlette_app`.
28+
"""
29+
# Set log level for urllib to WARNING to reduce noise
30+
logging.getLogger("urllib3").setLevel(logging.WARNING)
31+
32+
configure_otel_providers()
33+
34+
HTTPXClientInstrumentor().instrument(
35+
request_hook=request_hook,
36+
response_hook=response_hook,
37+
)

0 commit comments

Comments
 (0)