toad-eye 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -408
- package/dist/alerts/conditions.js +31 -6
- package/dist/alerts/grafana.js +6 -1
- package/dist/core/span-end-processor.d.ts +30 -0
- package/dist/core/span-end-processor.js +78 -0
- package/dist/core/tracer.js +35 -5
- package/dist/index.d.ts +1 -0
- package/dist/instrumentations/mcp.d.ts +12 -0
- package/dist/instrumentations/mcp.js +85 -0
- package/dist/mcp/client.d.ts +9 -0
- package/dist/mcp/client.js +137 -0
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/metrics.d.ts +3 -1
- package/dist/mcp/metrics.js +15 -1
- package/dist/mcp/middleware.d.ts +5 -1
- package/dist/mcp/middleware.js +23 -8
- package/dist/mcp/spans.js +3 -3
- package/dist/mcp/types.d.ts +2 -21
- package/dist/types/config.d.ts +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/metrics.d.ts +1 -0
- package/dist/types/metrics.js +1 -0
- package/dist/types/providers.d.ts +3 -1
- package/package.json +1 -1
- package/templates/alerts.example.yml +21 -0
- package/templates/grafana/dashboards/mcp-analytics.json +295 -0
- package/templates/grafana/dashboards/mcp-e2e.json +323 -0
- package/templates/grafana/dashboards/mcp-server.json +0 -19
package/README.md
CHANGED
|
@@ -1,170 +1,98 @@
|
|
|
1
|
-
# toad-eye
|
|
1
|
+
# toad-eye 🐸👁️
|
|
2
2
|
|
|
3
|
+
**Observability for MCP servers and LLM applications.**
|
|
4
|
+
|
|
5
|
+
One line of code. Full traces, metrics, and Grafana dashboards.
|
|
6
|
+
Self-hosted. Privacy-first. No vendor lock-in.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/toad-eye)
|
|
3
9
|

|
|
4
10
|

|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
8
|
-

|
|
9
|
-
[](https://www.npmjs.com/package/toad-eye)
|
|
10
11
|

|
|
11
12
|

|
|
12
13
|
|
|
13
|
-
**LLM observability in 3 commands. Auto-instrument, trace, alert, save money.**
|
|
14
|
-
|
|
15
|
-
You don't know what your LLMs cost, how slow they are, or why they fail. toad-eye gives you complete visibility with zero manual instrumentation.
|
|
16
|
-
|
|
17
14
|

|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**Prerequisites:** [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or Docker Engine + Compose plugin)
|
|
16
|
+
---
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
npm install toad-eye
|
|
25
|
-
npx toad-eye init # scaffold observability stack
|
|
26
|
-
npx toad-eye up # start Grafana + Prometheus + Jaeger
|
|
27
|
-
npx toad-eye demo # send mock LLM traffic, see data immediately
|
|
28
|
-
```
|
|
18
|
+
## Quick Start — MCP Server
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
Add observability to any MCP server in 2 lines:
|
|
31
21
|
|
|
32
22
|
```typescript
|
|
23
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
33
24
|
import { initObservability } from "toad-eye";
|
|
25
|
+
import { toadEyeMiddleware } from "toad-eye/mcp";
|
|
34
26
|
|
|
35
|
-
initObservability({
|
|
36
|
-
serviceName: "my-app",
|
|
37
|
-
instrument: ["openai", "anthropic"],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// That's it. Every SDK call is auto-traced — including streaming.
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
> ⚠️ By default, toad-eye records prompt and completion text in spans. Set `recordContent: false` for PII-sensitive workloads (healthcare, finance, legal).
|
|
44
|
-
|
|
45
|
-
Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin) to see your dashboards.
|
|
27
|
+
initObservability({ serviceName: "my-mcp-server" });
|
|
46
28
|
|
|
47
|
-
|
|
29
|
+
const server = new McpServer({ name: "my-server", version: "1.0.0" });
|
|
30
|
+
toadEyeMiddleware(server);
|
|
48
31
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| **MCP server monitoring** | One-line middleware for MCP servers — trace every tool call, resource read, prompt |
|
|
53
|
-
| **9 Grafana dashboards** | Overview, Cost, Latency, Errors, Model Comparison, FinOps, Provider Health, Agent, MCP |
|
|
54
|
-
| **Cost tracking** | Per-request USD cost, daily totals, projected monthly spend |
|
|
55
|
-
| **Budget guards** | Daily/per-user/per-model spend limits — warn, block, or auto-downgrade |
|
|
56
|
-
| **Privacy controls** | Built-in PII redaction (email, SSN, CC, phone), hashing, content masking |
|
|
32
|
+
// Every tool call, resource read, and prompt is now traced.
|
|
33
|
+
// Spans appear in Jaeger. Metrics flow to Prometheus. Dashboards ready in Grafana.
|
|
34
|
+
```
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
<summary>Advanced features</summary>
|
|
36
|
+
Privacy by default — tool arguments and results are NOT recorded unless you opt in:
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| **Tail sampling** | Keep 100% errors + slow traces, sample healthy traffic via OTel Collector |
|
|
68
|
-
| **Alerting** | Cost spikes, latency anomalies, error rate — via Telegram, Slack, email, webhook |
|
|
69
|
-
| **FinOps attribution** | Break down costs by team, user, feature, environment |
|
|
70
|
-
| **TTFT metric** | Time To First Token for streaming — separate from total duration |
|
|
71
|
-
| **Trace export** | Convert production Jaeger traces into regression test cases |
|
|
38
|
+
```typescript
|
|
39
|
+
toadEyeMiddleware(server, {
|
|
40
|
+
recordInputs: true,
|
|
41
|
+
redactKeys: ["apiKey", "token"],
|
|
42
|
+
});
|
|
43
|
+
```
|
|
72
44
|
|
|
73
|
-
|
|
45
|
+
Safe for stdio transport — OTel diagnostics are redirected to stderr.
|
|
74
46
|
|
|
75
|
-
##
|
|
47
|
+
## Quick Start — LLM Calls
|
|
76
48
|
|
|
77
|
-
|
|
49
|
+
Auto-instrument OpenAI, Anthropic, Gemini, and Vercel AI SDK — zero wrappers:
|
|
78
50
|
|
|
79
51
|
```typescript
|
|
52
|
+
import { initObservability } from "toad-eye";
|
|
53
|
+
|
|
80
54
|
initObservability({
|
|
81
55
|
serviceName: "my-app",
|
|
82
56
|
instrument: ["openai", "anthropic"],
|
|
83
57
|
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Supported SDKs:** OpenAI (`openai`), Anthropic (`@anthropic-ai/sdk`), Google GenAI (`@google/generative-ai`), Vercel AI SDK (`ai`)
|
|
87
|
-
|
|
88
|
-
Both regular and **streaming** calls are fully instrumented — spans, metrics, and cost tracking work transparently for `stream: true`.
|
|
89
58
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<details>
|
|
93
|
-
<summary>Manual instrumentation (custom providers)</summary>
|
|
94
|
-
|
|
95
|
-
Use `traceLLMCall` when you need to instrument a provider not in the auto-instrument list, or want explicit control over span data:
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import OpenAI from "openai";
|
|
99
|
-
import { initObservability, traceLLMCall } from "toad-eye";
|
|
100
|
-
|
|
101
|
-
initObservability({ serviceName: "my-app" });
|
|
102
|
-
|
|
103
|
-
const openai = new OpenAI();
|
|
104
|
-
|
|
105
|
-
const result = await traceLLMCall(
|
|
106
|
-
{ provider: "openai", model: "gpt-4o", prompt: "Hello" },
|
|
107
|
-
async () => {
|
|
108
|
-
const res = await openai.chat.completions.create({
|
|
109
|
-
model: "gpt-4o",
|
|
110
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
111
|
-
});
|
|
112
|
-
return {
|
|
113
|
-
completion: res.choices[0]?.message.content ?? "",
|
|
114
|
-
inputTokens: res.usage?.prompt_tokens ?? 0,
|
|
115
|
-
outputTokens: res.usage?.completion_tokens ?? 0,
|
|
116
|
-
// cost is auto-calculated from model + tokens if omitted
|
|
117
|
-
};
|
|
118
|
-
},
|
|
119
|
-
);
|
|
59
|
+
// Every SDK call is auto-traced — including streaming.
|
|
120
60
|
```
|
|
121
61
|
|
|
122
|
-
|
|
62
|
+
## Set Up the Stack
|
|
123
63
|
|
|
124
|
-
|
|
64
|
+
```bash
|
|
65
|
+
npm install toad-eye
|
|
66
|
+
npx toad-eye init # scaffold observability configs
|
|
67
|
+
npx toad-eye up # start Grafana + Prometheus + Jaeger + OTel Collector
|
|
68
|
+
npx toad-eye demo # send mock traffic — see data in Grafana immediately
|
|
69
|
+
```
|
|
125
70
|
|
|
126
|
-
|
|
71
|
+
Open [localhost:3100](http://localhost:3100) (Grafana, admin/admin). 11 dashboards are pre-built and ready.
|
|
127
72
|
|
|
128
|
-
|
|
73
|
+
> **Requires:** [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or Docker Engine + Compose plugin)
|
|
129
74
|
|
|
130
|
-
|
|
75
|
+
<!-- TODO: add Grafana dashboard screenshot here -->
|
|
131
76
|
|
|
132
|
-
|
|
133
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
134
|
-
import { initObservability } from "toad-eye";
|
|
135
|
-
import { toadEyeMiddleware } from "toad-eye/mcp";
|
|
77
|
+
## What You Get
|
|
136
78
|
|
|
137
|
-
|
|
79
|
+
**Instrumentation:** auto-instrument OpenAI, Anthropic, Gemini, Vercel AI SDK, and MCP servers — regular and streaming calls, with full cost tracking.
|
|
138
80
|
|
|
139
|
-
|
|
140
|
-
toadEyeMiddleware(server); // every tool call, resource read, prompt — traced
|
|
81
|
+
**11 Grafana dashboards:** Overview, Cost Breakdown, Latency Analysis, Errors, Model Comparison, FinOps Attribution, Provider Health, Agent Workflow, MCP Server, MCP End-to-End, MCP Tool Analytics.
|
|
141
82
|
|
|
142
|
-
|
|
143
|
-
return { content: [{ type: "text", text: String(eval(expression)) }] };
|
|
144
|
-
});
|
|
145
|
-
```
|
|
83
|
+
**Budget guards:** daily, per-user, per-model spend limits. Three modes — warn, block, or auto-downgrade to a cheaper model.
|
|
146
84
|
|
|
147
|
-
|
|
85
|
+
**Agent tracing:** structured ReAct tracing with multi-agent support, handoffs, and loop detection. Follows OTel GenAI semantic conventions.
|
|
148
86
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Privacy by default — tool arguments and results are NOT recorded unless you opt in:
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
toadEyeMiddleware(server, {
|
|
155
|
-
recordInputs: true,
|
|
156
|
-
recordOutputs: true,
|
|
157
|
-
redactKeys: ["apiKey", "token"],
|
|
158
|
-
});
|
|
159
|
-
```
|
|
87
|
+
**Privacy controls:** `recordContent: false` to disable prompt/completion recording, built-in PII redaction (email, SSN, CC, phone), SHA-256 hashing, key redaction.
|
|
160
88
|
|
|
161
|
-
|
|
89
|
+
**Alerting:** cost spikes, latency anomalies, error rate alerts via Telegram, Slack, email, or webhook.
|
|
162
90
|
|
|
163
|
-
|
|
91
|
+
**Semantic drift detection:** catch silent LLM quality degradation by comparing responses to a saved baseline via embeddings.
|
|
164
92
|
|
|
165
|
-
|
|
93
|
+
**Trace export:** convert production Jaeger traces into regression test cases for [toad-eval](https://github.com/vola-trebla/toad-eval).
|
|
166
94
|
|
|
167
|
-
|
|
95
|
+
## Budget Guards
|
|
168
96
|
|
|
169
97
|
```typescript
|
|
170
98
|
initObservability({
|
|
@@ -174,327 +102,71 @@ initObservability({
|
|
|
174
102
|
perUser: 5, // $5/day per user
|
|
175
103
|
perModel: { "gpt-4o": 30 }, // $30/day on GPT-4o
|
|
176
104
|
},
|
|
177
|
-
onBudgetExceeded: "block", //
|
|
105
|
+
onBudgetExceeded: "block", // or "warn" or "downgrade"
|
|
178
106
|
});
|
|
179
107
|
```
|
|
180
108
|
|
|
181
|
-
|
|
182
|
-
| ----------- | --------------------------------------------------- |
|
|
183
|
-
| `warn` | Log warning, continue normally |
|
|
184
|
-
| `block` | Throw `ToadBudgetExceededError` before LLM call |
|
|
185
|
-
| `downgrade` | Call `downgradeCallback` to switch to cheaper model |
|
|
186
|
-
|
|
187
|
-
## Agent observability
|
|
188
|
-
|
|
189
|
-
Structured tracing for ReAct agents with multi-agent support, handoffs, and loop detection. Span names follow [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/).
|
|
109
|
+
## Agent Observability
|
|
190
110
|
|
|
191
111
|
```typescript
|
|
192
112
|
import { traceAgentQuery } from "toad-eye";
|
|
193
113
|
|
|
194
114
|
const result = await traceAgentQuery(
|
|
195
|
-
{
|
|
196
|
-
query: "What's the weather like?",
|
|
197
|
-
agentName: "weather-bot", // → gen_ai.agent.name (OTel semconv)
|
|
198
|
-
},
|
|
115
|
+
{ query: "What's the weather?", agentName: "weather-bot" },
|
|
199
116
|
async (step) => {
|
|
200
117
|
step({ type: "think", stepNumber: 1, content: "Need weather data" });
|
|
201
118
|
const data = await getWeather();
|
|
202
|
-
step({
|
|
203
|
-
type: "act",
|
|
204
|
-
stepNumber: 2,
|
|
205
|
-
toolName: "get_weather",
|
|
206
|
-
toolType: "function",
|
|
207
|
-
});
|
|
119
|
+
step({ type: "act", stepNumber: 2, toolName: "get_weather" });
|
|
208
120
|
step({ type: "answer", stepNumber: 3, content: data.summary });
|
|
209
121
|
return { answer: data.summary };
|
|
210
122
|
},
|
|
211
123
|
);
|
|
212
|
-
// Produces
|
|
124
|
+
// Produces: invoke_agent weather-bot → execute_tool get_weather
|
|
213
125
|
```
|
|
214
126
|
|
|
215
|
-
|
|
216
|
-
<summary>Full ReAct pattern (think → act → observe → answer)</summary>
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
const result = await traceAgentQuery(
|
|
220
|
-
"Is anything dangerous near Earth?",
|
|
221
|
-
async (step) => {
|
|
222
|
-
step({
|
|
223
|
-
type: "think",
|
|
224
|
-
stepNumber: 1,
|
|
225
|
-
content: "I need to check asteroids",
|
|
226
|
-
});
|
|
227
|
-
const data = await callTool("near-earth-asteroids");
|
|
228
|
-
step({ type: "act", stepNumber: 2, toolName: "near-earth-asteroids" });
|
|
229
|
-
step({
|
|
230
|
-
type: "observe",
|
|
231
|
-
stepNumber: 3,
|
|
232
|
-
content: `${data.length} asteroids found`,
|
|
233
|
-
});
|
|
234
|
-
step({
|
|
235
|
-
type: "answer",
|
|
236
|
-
stepNumber: 4,
|
|
237
|
-
content: "7 asteroids passing safely",
|
|
238
|
-
});
|
|
239
|
-
return { answer: "7 asteroids passing safely" };
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
</details>
|
|
245
|
-
|
|
246
|
-
## FinOps attribution
|
|
247
|
-
|
|
248
|
-
Track costs by team, user, feature, or any business dimension:
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
initObservability({
|
|
252
|
-
serviceName: "my-app",
|
|
253
|
-
attributes: { team: "checkout", environment: "production" },
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
await traceLLMCall(
|
|
257
|
-
{
|
|
258
|
-
provider: "openai",
|
|
259
|
-
model: "gpt-4o",
|
|
260
|
-
prompt: "Summarize order",
|
|
261
|
-
attributes: { userId: "user-123", feature: "order-summary" },
|
|
262
|
-
},
|
|
263
|
-
() => callLLM(),
|
|
264
|
-
);
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
<details>
|
|
268
|
-
<summary>More features</summary>
|
|
269
|
-
|
|
270
|
-
### Shadow guardrails
|
|
271
|
-
|
|
272
|
-
Record what _would_ have been blocked without blocking. Tune thresholds on live traffic.
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { recordGuardResult } from "toad-eye";
|
|
276
|
-
|
|
277
|
-
recordGuardResult({
|
|
278
|
-
mode: "shadow",
|
|
279
|
-
passed: false,
|
|
280
|
-
ruleName: "pii_filter",
|
|
281
|
-
failureReason: "SSN detected in response",
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Semantic drift monitoring
|
|
286
|
-
|
|
287
|
-
Detect silent LLM quality degradation by comparing responses to a saved baseline via embeddings.
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
import { createDriftMonitor } from "toad-eye";
|
|
291
|
-
|
|
292
|
-
const monitor = createDriftMonitor({
|
|
293
|
-
embedding: { provider: "openai", apiKey: process.env.OPENAI_API_KEY! },
|
|
294
|
-
baselinePath: "./baseline.json",
|
|
295
|
-
sampleRate: 10,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
monitor.checkInBackground(response, "openai", "gpt-4o");
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Privacy controls
|
|
302
|
-
|
|
303
|
-
| Goal | Config |
|
|
304
|
-
| --------------------------- | -------------------------------- |
|
|
305
|
-
| Don't store text at all | `recordContent: false` |
|
|
306
|
-
| Remove common PII | `redactDefaults: true` |
|
|
307
|
-
| Remove custom patterns | `redactPatterns: [/regex/g]` |
|
|
308
|
-
| Store hashes only (no text) | `hashContent: true, salt: "..."` |
|
|
309
|
-
| Audit what was masked | `auditMasking: true` |
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
initObservability({
|
|
313
|
-
serviceName: "my-app",
|
|
314
|
-
redactDefaults: true, // built-in PII patterns (email, SSN, CC, phone)
|
|
315
|
-
recordContent: false, // disable prompt/completion recording
|
|
316
|
-
hashContent: true, // SHA-256 hash instead of plain text
|
|
317
|
-
salt: "my-secret", // prevent rainbow table attacks
|
|
318
|
-
auditMasking: true, // log what was masked (debug only)
|
|
319
|
-
redactPatterns: [/custom-pattern/g], // additional regex redaction
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Session tracking
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
initObservability({
|
|
327
|
-
serviceName: "my-app",
|
|
328
|
-
sessionId: "static-session-id",
|
|
329
|
-
sessionExtractor: () => getCurrentSessionId(), // or dynamic
|
|
330
|
-
});
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### Built-in cost tracking
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
import { setCustomPricing } from "toad-eye";
|
|
337
|
-
|
|
338
|
-
setCustomPricing({
|
|
339
|
-
"my-fine-tuned-model": { inputPer1M: 5, outputPer1M: 15 },
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Alerting
|
|
344
|
-
|
|
345
|
-
```yaml
|
|
346
|
-
alerts:
|
|
347
|
-
- name: cost_spike
|
|
348
|
-
metric: gen_ai.client.request.cost
|
|
349
|
-
condition: sum_1h > 10
|
|
350
|
-
channels: [slack]
|
|
351
|
-
|
|
352
|
-
- name: error_rate
|
|
353
|
-
metric: gen_ai.client.errors
|
|
354
|
-
condition: ratio_15m > 0.05
|
|
355
|
-
channels: [telegram]
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
Channels: Telegram, Slack webhook, HTTP webhook, email (SMTP).
|
|
359
|
-
|
|
360
|
-
### Trace-to-dataset export
|
|
361
|
-
|
|
362
|
-
Convert production Jaeger traces into test cases:
|
|
127
|
+
## CLI
|
|
363
128
|
|
|
364
|
-
```bash
|
|
365
|
-
npx toad-eye export-trace <trace_id> --output ./evals/
|
|
366
129
|
```
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
initObservability({
|
|
374
|
-
serviceName: "my-app",
|
|
375
|
-
apiKey: "toad_xxxxxxxx",
|
|
376
|
-
instrument: ["openai"],
|
|
377
|
-
});
|
|
130
|
+
npx toad-eye init [--force] Scaffold Docker Compose + observability configs
|
|
131
|
+
npx toad-eye up Start the stack
|
|
132
|
+
npx toad-eye down Stop the stack
|
|
133
|
+
npx toad-eye status Show running services and URLs
|
|
134
|
+
npx toad-eye demo Send mock LLM traffic to Grafana
|
|
135
|
+
npx toad-eye export-trace <id> Export a Jaeger trace to toad-eval YAML
|
|
378
136
|
```
|
|
379
137
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
</details>
|
|
383
|
-
|
|
384
|
-
## Grafana dashboards
|
|
385
|
-
|
|
386
|
-
9 pre-built dashboards auto-provisioned on `npx toad-eye init`:
|
|
387
|
-
|
|
388
|
-
| Dashboard | What it shows |
|
|
389
|
-
| ---------------------- | ---------------------------------------------------------------- |
|
|
390
|
-
| **Overview** | Request rate, error rate, latency p50/p95, cost, totals |
|
|
391
|
-
| **Cost Breakdown** | Spend by provider/model, daily trend, projected monthly |
|
|
392
|
-
| **Latency Analysis** | p50/p95/p99 by model, distribution histogram, TTFT |
|
|
393
|
-
| **Error Drill-down** | Error rate by provider/model, error vs success |
|
|
394
|
-
| **Model Comparison** | Latency vs cost vs error rate vs throughput per model |
|
|
395
|
-
| **FinOps Attribution** | Cost by team/user/feature, projected spend, what-if table |
|
|
396
|
-
| **Provider Health** | Provider status (healthy/degraded/down), uptime, error breakdown |
|
|
397
|
-
| **Agent Workflow** | Steps per query, tool usage frequency, step type breakdown |
|
|
398
|
-
| **MCP Server** | Tool call rate, duration p50/p95, errors by tool, resource reads |
|
|
138
|
+
## Architecture
|
|
399
139
|
|
|
400
|
-
|
|
140
|
+

|
|
401
141
|
|
|
402
|
-
|
|
142
|
+
## Imports
|
|
403
143
|
|
|
404
144
|
```typescript
|
|
405
145
|
import { initObservability, traceLLMCall } from "toad-eye"; // core
|
|
406
|
-
import {
|
|
146
|
+
import { toadEyeMiddleware } from "toad-eye/mcp"; // MCP server middleware
|
|
147
|
+
import { AlertManager } from "toad-eye/alerts"; // alerting engine
|
|
407
148
|
import { createDriftMonitor } from "toad-eye/drift"; // semantic drift
|
|
408
149
|
import { exportTrace } from "toad-eye/export"; // trace → YAML
|
|
409
150
|
import { ToadEyeAISpanProcessor, withToadEye } from "toad-eye/vercel"; // Vercel AI SDK
|
|
410
151
|
```
|
|
411
152
|
|
|
412
|
-
> Everything is also re-exported from the main `"toad-eye"` entry point for convenience.
|
|
413
|
-
|
|
414
|
-
## CLI
|
|
415
|
-
|
|
416
|
-
| Command | Description |
|
|
417
|
-
| -------------------------------- | ----------------------------------------------- |
|
|
418
|
-
| `npx toad-eye init [--force]` | Scaffold Docker Compose + observability configs |
|
|
419
|
-
| `npx toad-eye up` | Start the stack |
|
|
420
|
-
| `npx toad-eye down` | Stop the stack |
|
|
421
|
-
| `npx toad-eye status` | Show running services and URLs |
|
|
422
|
-
| `npx toad-eye demo` | Send mock LLM traffic to see data in Grafana |
|
|
423
|
-
| `npx toad-eye export-trace <id>` | Export a Jaeger trace to toad-eval YAML |
|
|
424
|
-
|
|
425
|
-
## Architecture
|
|
426
|
-
|
|
427
|
-

|
|
428
|
-
|
|
429
|
-
## What it tracks
|
|
430
|
-
|
|
431
|
-
<details>
|
|
432
|
-
<summary>Metrics & span attributes</summary>
|
|
433
|
-
|
|
434
|
-
### Metrics (OTel GenAI semconv)
|
|
435
|
-
|
|
436
|
-
| Metric | Type | Description |
|
|
437
|
-
| -------------------------------------------- | --------- | --------------------------- |
|
|
438
|
-
| `gen_ai.client.operation.duration` | Histogram | Request latency (ms) |
|
|
439
|
-
| `gen_ai.client.time_to_first_token` | Histogram | TTFT for streaming (ms) |
|
|
440
|
-
| `gen_ai.client.request.cost` | Histogram | Cost per request (USD) |
|
|
441
|
-
| `gen_ai.client.token.usage` | Counter | Total tokens consumed |
|
|
442
|
-
| `gen_ai.client.requests` | Counter | Total requests |
|
|
443
|
-
| `gen_ai.client.errors` | Counter | Total failed requests |
|
|
444
|
-
| `gen_ai.agent.steps_per_query` | Histogram | Agent steps per query |
|
|
445
|
-
| `gen_ai.agent.tool_usage` | Counter | Agent tool invocations |
|
|
446
|
-
| `gen_ai.toad_eye.guard.evaluations` | Counter | Guard evaluations per rule |
|
|
447
|
-
| `gen_ai.toad_eye.guard.would_block` | Counter | Would-have-blocked per rule |
|
|
448
|
-
| `gen_ai.toad_eye.semantic_drift` | Histogram | Drift from baseline (0..1) |
|
|
449
|
-
| `gen_ai.toad_eye.budget.exceeded` | Counter | Budget exceeded events |
|
|
450
|
-
| `gen_ai.toad_eye.budget.blocked` | Counter | LLM calls blocked by budget |
|
|
451
|
-
| `gen_ai.toad_eye.budget.downgraded` | Counter | LLM calls downgraded |
|
|
452
|
-
| `gen_ai.toad_eye.response.empty` | Counter | Empty/whitespace responses |
|
|
453
|
-
| `gen_ai.toad_eye.response.latency_per_token` | Histogram | Generation speed (ms/token) |
|
|
454
|
-
|
|
455
|
-
### Span attributes (OTel GenAI semconv)
|
|
456
|
-
|
|
457
|
-
| Attribute | Description |
|
|
458
|
-
| ---------------------------------- | ---------------------------------------------------------------------- |
|
|
459
|
-
| `gen_ai.operation.name` | `chat`, `invoke_agent`, `execute_tool`, `tools/call`, `resources/read` |
|
|
460
|
-
| `gen_ai.provider.name` | `anthropic`, `gemini`, `openai` |
|
|
461
|
-
| `gen_ai.request.model` | Model identifier |
|
|
462
|
-
| `gen_ai.usage.input_tokens` | Tokens in the prompt |
|
|
463
|
-
| `gen_ai.usage.output_tokens` | Tokens in the completion |
|
|
464
|
-
| `gen_ai.request.temperature` | Temperature parameter |
|
|
465
|
-
| `gen_ai.agent.name` | Agent name (for agent spans) |
|
|
466
|
-
| `gen_ai.agent.id` | Agent identifier |
|
|
467
|
-
| `gen_ai.tool.name` | Tool name for execute_tool spans |
|
|
468
|
-
| `gen_ai.tool.type` | `function`, `extension`, `retrieval`, `builtin` |
|
|
469
|
-
| `gen_ai.toad_eye.cost` | Cost in USD |
|
|
470
|
-
| `gen_ai.toad_eye.agent.step.type` | ReAct step: think/act/observe/answer/handoff |
|
|
471
|
-
| `gen_ai.toad_eye.agent.handoff.to` | Target agent for handoff steps |
|
|
472
|
-
| `gen_ai.toad_eye.agent.loop_count` | Agent loop iterations (observe→think) |
|
|
473
|
-
| `gen_ai.toad_eye.guard.mode` | Guard mode: shadow/enforce |
|
|
474
|
-
| `session.id` | Session identifier (if configured) |
|
|
475
|
-
|
|
476
|
-
</details>
|
|
477
|
-
|
|
478
153
|
## Services
|
|
479
154
|
|
|
480
|
-
|
|
481
|
-
| -------------- | ------------------------------------------------------- |
|
|
482
|
-
| Grafana | [localhost:3100](http://localhost:3100) (admin / admin) |
|
|
483
|
-
| Jaeger UI | [localhost:16686](http://localhost:16686) |
|
|
484
|
-
| Prometheus | [localhost:9090](http://localhost:9090) |
|
|
485
|
-
| OTel Collector | [localhost:4318](http://localhost:4318) |
|
|
155
|
+
After `npx toad-eye up`:
|
|
486
156
|
|
|
487
|
-
|
|
157
|
+
```
|
|
158
|
+
Grafana http://localhost:3100 (admin / admin)
|
|
159
|
+
Jaeger UI http://localhost:16686
|
|
160
|
+
Prometheus http://localhost:9090
|
|
161
|
+
OTel Collector http://localhost:4318
|
|
162
|
+
```
|
|
488
163
|
|
|
489
|
-
|
|
164
|
+
## OTel Compatibility
|
|
490
165
|
|
|
491
|
-
|
|
166
|
+
toad-eye follows [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Traces work natively with Jaeger, Datadog, Grafana Tempo, Honeycomb, SigNoz, Arize Phoenix, and any OTel-compatible backend.
|
|
492
167
|
|
|
493
|
-
|
|
168
|
+
Full metrics and span attribute reference: [COMPATIBILITY.md](https://github.com/vola-trebla/toad-eye/blob/main/packages/instrumentation/COMPATIBILITY.md)
|
|
494
169
|
|
|
495
|
-
## Tech
|
|
170
|
+
## Tech Stack
|
|
496
171
|
|
|
497
|
-
|
|
498
|
-
- Hono (demo server + cloud ingestion server)
|
|
499
|
-
- Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector)
|
|
500
|
-
- Vitest (252+ tests)
|
|
172
|
+
TypeScript · OpenTelemetry SDK 2.x · Hono · Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector) · Vitest (273+ tests)
|
|
@@ -5,6 +5,12 @@ const METRIC_MAP = {
|
|
|
5
5
|
"gen_ai.client.requests": "gen_ai_client_requests_total",
|
|
6
6
|
"gen_ai.client.errors": "gen_ai_client_errors_total",
|
|
7
7
|
"gen_ai.client.token.usage": "gen_ai_client_token_usage_total",
|
|
8
|
+
// MCP metrics
|
|
9
|
+
"gen_ai.mcp.tool.calls": "gen_ai_mcp_tool_calls_total",
|
|
10
|
+
"gen_ai.mcp.tool.errors": "gen_ai_mcp_tool_errors_total",
|
|
11
|
+
"gen_ai.mcp.tool.duration": "gen_ai_mcp_tool_duration",
|
|
12
|
+
"gen_ai.mcp.resource.reads": "gen_ai_mcp_resource_reads_total",
|
|
13
|
+
"gen_ai.mcp.tool.callers": "gen_ai_mcp_tool_callers_total",
|
|
8
14
|
// Legacy names (backward compat)
|
|
9
15
|
"llm.request.cost": "gen_ai_client_request_cost_USD_sum",
|
|
10
16
|
"llm.request.duration": "gen_ai_client_operation_duration_milliseconds",
|
|
@@ -15,6 +21,9 @@ const METRIC_MAP = {
|
|
|
15
21
|
function toPromMetric(metric) {
|
|
16
22
|
return METRIC_MAP[metric] ?? metric.replace(/\./g, "_");
|
|
17
23
|
}
|
|
24
|
+
function isMcpMetric(metric) {
|
|
25
|
+
return metric.startsWith("gen_ai.mcp.");
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Parse condition string into structured form.
|
|
20
29
|
*
|
|
@@ -65,7 +74,10 @@ function buildPromQL(metric, operator, window) {
|
|
|
65
74
|
}
|
|
66
75
|
function buildTopModelsQuery(metric, window) {
|
|
67
76
|
const m = toPromMetric(metric);
|
|
68
|
-
|
|
77
|
+
const groupBy = isMcpMetric(metric)
|
|
78
|
+
? "gen_ai_tool_name"
|
|
79
|
+
: "gen_ai_request_model";
|
|
80
|
+
return `topk(5, sum by (${groupBy}) (increase(${m}[${window}])))`;
|
|
69
81
|
}
|
|
70
82
|
function matches(value, comparator, threshold) {
|
|
71
83
|
switch (comparator) {
|
|
@@ -94,10 +106,13 @@ async function queryTopModels(prometheusUrl, metric, window) {
|
|
|
94
106
|
const res = await fetch(url);
|
|
95
107
|
if (!res.ok)
|
|
96
108
|
return [];
|
|
109
|
+
const labelKey = isMcpMetric(metric)
|
|
110
|
+
? "gen_ai_tool_name"
|
|
111
|
+
: "gen_ai_request_model";
|
|
97
112
|
const data = (await res.json());
|
|
98
113
|
return data.data.result
|
|
99
114
|
.map((r) => ({
|
|
100
|
-
model: r.metric
|
|
115
|
+
model: r.metric[labelKey] ?? "unknown",
|
|
101
116
|
value: parseFloat(r.value[1]),
|
|
102
117
|
}))
|
|
103
118
|
.sort((a, b) => b.value - a.value);
|
|
@@ -113,9 +128,19 @@ async function evalP95Baseline(prometheusUrl, metric, window, baselineWindow) {
|
|
|
113
128
|
return ((currentP95 - baselineP95) / baselineP95) * 100;
|
|
114
129
|
}
|
|
115
130
|
/** Evaluate error ratio: errors / requests over window */
|
|
116
|
-
async function evalErrorRatio(prometheusUrl, window) {
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
async function evalErrorRatio(prometheusUrl, metric, window) {
|
|
132
|
+
let errorMetric;
|
|
133
|
+
let requestMetric;
|
|
134
|
+
if (isMcpMetric(metric)) {
|
|
135
|
+
errorMetric = "gen_ai_mcp_tool_errors_total";
|
|
136
|
+
requestMetric = "gen_ai_mcp_tool_calls_total";
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
errorMetric = "gen_ai_client_errors_total";
|
|
140
|
+
requestMetric = "gen_ai_client_requests_total";
|
|
141
|
+
}
|
|
142
|
+
const errors = await queryScalar(prometheusUrl, `sum(increase(${errorMetric}[${window}]))`);
|
|
143
|
+
const requests = await queryScalar(prometheusUrl, `sum(increase(${requestMetric}[${window}]))`);
|
|
119
144
|
if (requests === 0)
|
|
120
145
|
return 0;
|
|
121
146
|
return errors / requests;
|
|
@@ -128,7 +153,7 @@ export async function evaluateCondition(prometheusUrl, rule) {
|
|
|
128
153
|
value = await evalP95Baseline(prometheusUrl, rule.metric, parsed.window, parsed.baselineWindow);
|
|
129
154
|
}
|
|
130
155
|
else if (parsed.operator === "ratio") {
|
|
131
|
-
value = await evalErrorRatio(prometheusUrl, parsed.window);
|
|
156
|
+
value = await evalErrorRatio(prometheusUrl, rule.metric, parsed.window);
|
|
132
157
|
}
|
|
133
158
|
else {
|
|
134
159
|
const promql = buildPromQL(rule.metric, parsed.operator, parsed.window);
|