toad-eye 2.6.1 → 2.8.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 -402
- package/dist/alerts/conditions.js +31 -6
- package/dist/alerts/grafana.js +6 -1
- package/dist/cli.js +11 -4
- package/dist/core/tracer.js +22 -2
- 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 +4 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/metrics.d.ts +5 -1
- package/dist/mcp/metrics.js +36 -5
- package/dist/mcp/middleware.d.ts +11 -1
- package/dist/mcp/middleware.js +54 -9
- package/dist/mcp/spans.d.ts +18 -5
- package/dist/mcp/spans.js +44 -24
- package/dist/mcp/types.d.ts +4 -18
- 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 +13 -4
- 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 +32 -12
- package/templates/otel-collector.yml +2 -2
package/dist/mcp/spans.js
CHANGED
|
@@ -1,56 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OTel span creation for MCP operations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - tools/call →
|
|
6
|
-
* - resources/read →
|
|
7
|
-
* - prompts/get →
|
|
8
|
-
* - sampling/createMessage →
|
|
4
|
+
* Follows OTel MCP semantic conventions (PR #2083):
|
|
5
|
+
* - tools/call → `tools/call {tool_name}`
|
|
6
|
+
* - resources/read → `resources/read {uri}`
|
|
7
|
+
* - prompts/get → `prompts/get {name}`
|
|
8
|
+
* - sampling/createMessage → `sampling/createMessage {model}`
|
|
9
9
|
*/
|
|
10
10
|
import { trace, SpanStatusCode, SpanKind, } from "@opentelemetry/api";
|
|
11
11
|
const TRACER_NAME = "toad-eye-mcp";
|
|
12
12
|
const tracer = trace.getTracer(TRACER_NAME);
|
|
13
|
+
/** MCP method names per OTel semconv. */
|
|
14
|
+
export const MCP_METHODS = {
|
|
15
|
+
TOOLS_CALL: "tools/call",
|
|
16
|
+
RESOURCES_READ: "resources/read",
|
|
17
|
+
PROMPTS_GET: "prompts/get",
|
|
18
|
+
SAMPLING_CREATE_MESSAGE: "sampling/createMessage",
|
|
19
|
+
};
|
|
20
|
+
/** Build common MCP attributes shared across all span types. */
|
|
21
|
+
function baseAttrs(method, options) {
|
|
22
|
+
const attrs = {
|
|
23
|
+
"gen_ai.operation.name": method,
|
|
24
|
+
"mcp.method.name": method,
|
|
25
|
+
"mcp.server.name": options.serverName,
|
|
26
|
+
"mcp.server.version": options.serverVersion,
|
|
27
|
+
};
|
|
28
|
+
if (options.sessionId)
|
|
29
|
+
attrs["mcp.session.id"] = options.sessionId;
|
|
30
|
+
if (options.protocolVersion)
|
|
31
|
+
attrs["mcp.protocol.version"] = options.protocolVersion;
|
|
32
|
+
if (options.networkTransport)
|
|
33
|
+
attrs["network.transport"] = options.networkTransport;
|
|
34
|
+
return attrs;
|
|
35
|
+
}
|
|
13
36
|
export function startToolSpan(toolName, options) {
|
|
14
|
-
return tracer.startSpan(
|
|
15
|
-
kind: SpanKind.
|
|
37
|
+
return tracer.startSpan(`${MCP_METHODS.TOOLS_CALL} ${toolName}`, {
|
|
38
|
+
kind: SpanKind.SERVER,
|
|
16
39
|
attributes: {
|
|
17
|
-
|
|
40
|
+
...baseAttrs(MCP_METHODS.TOOLS_CALL, options),
|
|
18
41
|
"gen_ai.tool.name": toolName,
|
|
19
|
-
"mcp.server.name": options.serverName,
|
|
20
|
-
"mcp.server.version": options.serverVersion,
|
|
21
42
|
},
|
|
22
43
|
}, options.parentContext);
|
|
23
44
|
}
|
|
24
45
|
export function startResourceSpan(uri, options) {
|
|
25
|
-
return tracer.startSpan(
|
|
26
|
-
kind: SpanKind.
|
|
46
|
+
return tracer.startSpan(`${MCP_METHODS.RESOURCES_READ} ${uri}`, {
|
|
47
|
+
kind: SpanKind.SERVER,
|
|
27
48
|
attributes: {
|
|
28
|
-
|
|
49
|
+
...baseAttrs(MCP_METHODS.RESOURCES_READ, options),
|
|
29
50
|
"gen_ai.data_source.id": uri,
|
|
30
|
-
"mcp.server.name": options.serverName,
|
|
31
|
-
"mcp.server.version": options.serverVersion,
|
|
32
51
|
},
|
|
33
52
|
}, options.parentContext);
|
|
34
53
|
}
|
|
35
54
|
export function startPromptSpan(promptName, options) {
|
|
36
|
-
return tracer.startSpan(
|
|
37
|
-
kind: SpanKind.
|
|
55
|
+
return tracer.startSpan(`${MCP_METHODS.PROMPTS_GET} ${promptName}`, {
|
|
56
|
+
kind: SpanKind.SERVER,
|
|
38
57
|
attributes: {
|
|
39
|
-
|
|
58
|
+
...baseAttrs(MCP_METHODS.PROMPTS_GET, options),
|
|
40
59
|
"gen_ai.prompt.name": promptName,
|
|
41
|
-
"mcp.server.name": options.serverName,
|
|
42
|
-
"mcp.server.version": options.serverVersion,
|
|
43
60
|
},
|
|
44
61
|
}, options.parentContext);
|
|
45
62
|
}
|
|
46
63
|
export function startSamplingSpan(model, options) {
|
|
47
|
-
return tracer.startSpan(
|
|
64
|
+
return tracer.startSpan(`${MCP_METHODS.SAMPLING_CREATE_MESSAGE} ${model}`, {
|
|
48
65
|
kind: SpanKind.CLIENT,
|
|
49
66
|
attributes: {
|
|
50
|
-
|
|
67
|
+
...baseAttrs(MCP_METHODS.SAMPLING_CREATE_MESSAGE, options),
|
|
51
68
|
"gen_ai.request.model": model,
|
|
52
|
-
"mcp.server.name": options.serverName,
|
|
53
|
-
"mcp.server.version": options.serverVersion,
|
|
54
69
|
},
|
|
55
70
|
}, options.parentContext);
|
|
56
71
|
}
|
|
@@ -63,5 +78,10 @@ export function endSpanError(span, error) {
|
|
|
63
78
|
const errorType = error instanceof Error ? error.constructor.name : "UnknownError";
|
|
64
79
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
65
80
|
span.setAttribute("error.type", errorType);
|
|
81
|
+
// JSON-RPC error code if available (MCP SDK errors often carry a code)
|
|
82
|
+
const code = error?.code;
|
|
83
|
+
if (typeof code === "number") {
|
|
84
|
+
span.setAttribute("rpc.response.status_code", code);
|
|
85
|
+
}
|
|
66
86
|
span.end();
|
|
67
87
|
}
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { Span } from "@opentelemetry/api";
|
|
2
2
|
/** Configuration for toadEyeMiddleware(). */
|
|
3
3
|
export interface ToadMcpOptions {
|
|
4
|
-
/** Service name for OTel resource. Defaults to MCP server name. */
|
|
5
|
-
readonly serviceName?: string | undefined;
|
|
6
|
-
/** OTLP endpoint. Uses existing OTel config if initObservability() was called. */
|
|
7
|
-
readonly endpoint?: string | undefined;
|
|
8
4
|
/** Record tool call arguments in spans. Default: false (privacy). */
|
|
9
5
|
readonly recordInputs?: boolean | undefined;
|
|
10
6
|
/** Record tool call results in spans. Default: false (privacy). */
|
|
11
7
|
readonly recordOutputs?: boolean | undefined;
|
|
12
|
-
/** Keys to redact from recorded arguments. */
|
|
8
|
+
/** Keys to redact from recorded arguments (recursive). */
|
|
13
9
|
readonly redactKeys?: readonly string[] | undefined;
|
|
14
|
-
/** Max payload size in
|
|
10
|
+
/** Max payload size in characters before truncation. Default: 4096. */
|
|
15
11
|
readonly maxPayloadSize?: number | undefined;
|
|
12
|
+
/** Explicit session ID. Auto-generated if not provided. */
|
|
13
|
+
readonly sessionId?: string | undefined;
|
|
16
14
|
/** Extract W3C traceparent from _meta field. Default: true. */
|
|
17
15
|
readonly propagateContext?: boolean | undefined;
|
|
18
16
|
/** Hook called on each tool call span. Add custom attributes here. */
|
|
@@ -20,15 +18,3 @@ export interface ToadMcpOptions {
|
|
|
20
18
|
/** Hook called on each resource read span. */
|
|
21
19
|
readonly onResourceRead?: ((span: Span, uri: string) => void) | undefined;
|
|
22
20
|
}
|
|
23
|
-
/** OTel attributes set on MCP spans. */
|
|
24
|
-
export interface McpSpanAttributes {
|
|
25
|
-
readonly "gen_ai.operation.name": string;
|
|
26
|
-
readonly "gen_ai.tool.name"?: string;
|
|
27
|
-
readonly "gen_ai.tool.call.id"?: string;
|
|
28
|
-
readonly "gen_ai.data_source.id"?: string;
|
|
29
|
-
readonly "gen_ai.prompt.name"?: string;
|
|
30
|
-
readonly "mcp.server.name": string;
|
|
31
|
-
readonly "mcp.server.version": string;
|
|
32
|
-
readonly "mcp.transport"?: "stdio" | "sse" | "streamable-http";
|
|
33
|
-
readonly "mcp.session.id"?: string;
|
|
34
|
-
}
|
package/dist/types/metrics.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare const GEN_AI_METRICS: {
|
|
|
31
31
|
readonly MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors";
|
|
32
32
|
readonly MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads";
|
|
33
33
|
readonly MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active";
|
|
34
|
+
readonly MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers";
|
|
34
35
|
};
|
|
35
36
|
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
36
37
|
export declare const LLM_METRICS: {
|
package/dist/types/metrics.js
CHANGED
|
@@ -39,6 +39,7 @@ export const GEN_AI_METRICS = {
|
|
|
39
39
|
MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors",
|
|
40
40
|
MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads",
|
|
41
41
|
MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active",
|
|
42
|
+
MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers",
|
|
42
43
|
};
|
|
43
44
|
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
44
45
|
export const LLM_METRICS = {
|
|
@@ -6,5 +6,7 @@ export type LLMProvider = "anthropic" | "gemini" | "openai" | (string & {});
|
|
|
6
6
|
/**
|
|
7
7
|
* All instrumentable targets — includes LLM providers + framework SDKs.
|
|
8
8
|
* 'ai' = Vercel AI SDK (uses SpanProcessor, not monkey-patching).
|
|
9
|
+
* 'mcp' = MCP SDK McpServer (patches prototype for auto-instrumentation).
|
|
10
|
+
* 'mcp-client' = MCP SDK Client (patches callTool/readResource/getPrompt for distributed tracing).
|
|
9
11
|
*/
|
|
10
|
-
export type InstrumentTarget = LLMProvider | "ai";
|
|
12
|
+
export type InstrumentTarget = LLMProvider | "ai" | "mcp" | "mcp-client";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toad-eye",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.8.0",
|
|
4
|
+
"description": "Observability for MCP servers and LLM applications — traces, metrics, dashboards. One-line instrumentation for OpenAI, Anthropic, Gemini, and MCP. Self-hosted with Grafana + Prometheus + Jaeger.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"build": "tsc -p tsconfig.build.json",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
|
-
"prepublishOnly": "npm run build"
|
|
52
|
+
"prepublishOnly": "test -s README.md || { echo '❌ README.md is missing or empty'; exit 1; } && npm run build"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|
|
55
55
|
"type": "git",
|
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
"homepage": "https://github.com/vola-trebla/toad-eye#readme",
|
|
59
59
|
"author": "albertalov",
|
|
60
60
|
"keywords": [
|
|
61
|
+
"mcp",
|
|
62
|
+
"model-context-protocol",
|
|
63
|
+
"mcp-server",
|
|
64
|
+
"mcp-observability",
|
|
65
|
+
"mcp-monitoring",
|
|
61
66
|
"opentelemetry",
|
|
62
67
|
"llm",
|
|
63
68
|
"instrumentation",
|
|
@@ -71,9 +76,13 @@
|
|
|
71
76
|
"cost-tracking",
|
|
72
77
|
"auto-instrumentation",
|
|
73
78
|
"gen-ai",
|
|
79
|
+
"ai-agent",
|
|
80
|
+
"tool-calling",
|
|
74
81
|
"budget-guards",
|
|
75
82
|
"finops",
|
|
76
|
-
"streaming"
|
|
83
|
+
"streaming",
|
|
84
|
+
"jaeger",
|
|
85
|
+
"prometheus"
|
|
77
86
|
],
|
|
78
87
|
"license": "ISC",
|
|
79
88
|
"dependencies": {
|
|
@@ -52,3 +52,24 @@ alerts:
|
|
|
52
52
|
condition: ratio_5m > 0.2
|
|
53
53
|
channels: [slack]
|
|
54
54
|
cooldown: 15
|
|
55
|
+
|
|
56
|
+
# --- MCP Server alerts ---
|
|
57
|
+
|
|
58
|
+
# MCP tool error rate: >10% over 5 minutes
|
|
59
|
+
- name: mcp_tool_error_rate
|
|
60
|
+
metric: gen_ai.mcp.tool.errors
|
|
61
|
+
condition: ratio_5m > 0.1
|
|
62
|
+
channels: [slack]
|
|
63
|
+
|
|
64
|
+
# MCP tool latency spike: avg > 2 seconds over 5 minutes
|
|
65
|
+
- name: mcp_tool_latency
|
|
66
|
+
metric: gen_ai.mcp.tool.duration
|
|
67
|
+
condition: avg_5m > 2000
|
|
68
|
+
channels: [slack]
|
|
69
|
+
|
|
70
|
+
# MCP tool call spike: >100 calls in 5 minutes
|
|
71
|
+
- name: mcp_tool_call_spike
|
|
72
|
+
metric: gen_ai.mcp.tool.calls
|
|
73
|
+
condition: sum_5m > 100
|
|
74
|
+
channels: [slack]
|
|
75
|
+
cooldown: 15
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
{
|
|
2
|
+
"annotations": {
|
|
3
|
+
"list": []
|
|
4
|
+
},
|
|
5
|
+
"editable": true,
|
|
6
|
+
"fiscalYearStartMonth": 0,
|
|
7
|
+
"graphTooltip": 1,
|
|
8
|
+
"id": null,
|
|
9
|
+
"links": [],
|
|
10
|
+
"panels": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Tool Popularity Ranking",
|
|
13
|
+
"description": "Most called tools by total invocations",
|
|
14
|
+
"type": "bargauge",
|
|
15
|
+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
|
|
16
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
17
|
+
"targets": [
|
|
18
|
+
{
|
|
19
|
+
"expr": "topk(10, sum by (gen_ai_tool_name) (increase(gen_ai_mcp_tool_calls_total[1h])))",
|
|
20
|
+
"legendFormat": "{{gen_ai_tool_name}}",
|
|
21
|
+
"refId": "A",
|
|
22
|
+
"instant": true
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"fieldConfig": {
|
|
26
|
+
"defaults": {
|
|
27
|
+
"unit": "short",
|
|
28
|
+
"decimals": 0,
|
|
29
|
+
"thresholds": {
|
|
30
|
+
"steps": [
|
|
31
|
+
{ "color": "blue", "value": null },
|
|
32
|
+
{ "color": "green", "value": 10 },
|
|
33
|
+
{ "color": "yellow", "value": 100 },
|
|
34
|
+
{ "color": "red", "value": 500 }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"overrides": []
|
|
39
|
+
},
|
|
40
|
+
"options": {
|
|
41
|
+
"orientation": "horizontal",
|
|
42
|
+
"displayMode": "gradient",
|
|
43
|
+
"showUnfilled": true,
|
|
44
|
+
"minVizWidth": 0,
|
|
45
|
+
"minVizHeight": 10
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"title": "Unique Callers per Tool",
|
|
50
|
+
"description": "Number of distinct sessions calling each tool",
|
|
51
|
+
"type": "bargauge",
|
|
52
|
+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
|
|
53
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
54
|
+
"targets": [
|
|
55
|
+
{
|
|
56
|
+
"expr": "count by (gen_ai_tool_name) (sum by (gen_ai_tool_name, mcp_session_id) (gen_ai_mcp_tool_callers_total))",
|
|
57
|
+
"legendFormat": "{{gen_ai_tool_name}}",
|
|
58
|
+
"refId": "A",
|
|
59
|
+
"instant": true
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"fieldConfig": {
|
|
63
|
+
"defaults": {
|
|
64
|
+
"unit": "short",
|
|
65
|
+
"decimals": 0,
|
|
66
|
+
"thresholds": {
|
|
67
|
+
"steps": [
|
|
68
|
+
{ "color": "purple", "value": null },
|
|
69
|
+
{ "color": "blue", "value": 5 },
|
|
70
|
+
{ "color": "green", "value": 20 }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"overrides": []
|
|
75
|
+
},
|
|
76
|
+
"options": {
|
|
77
|
+
"orientation": "horizontal",
|
|
78
|
+
"displayMode": "gradient",
|
|
79
|
+
"showUnfilled": true,
|
|
80
|
+
"minVizWidth": 0,
|
|
81
|
+
"minVizHeight": 10
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"title": "Tool Usage Over Time",
|
|
86
|
+
"description": "Call rate per tool — spot trends and spikes",
|
|
87
|
+
"type": "timeseries",
|
|
88
|
+
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 },
|
|
89
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
90
|
+
"targets": [
|
|
91
|
+
{
|
|
92
|
+
"expr": "sum by (gen_ai_tool_name) (rate(gen_ai_mcp_tool_calls_total[5m]))",
|
|
93
|
+
"legendFormat": "{{gen_ai_tool_name}}",
|
|
94
|
+
"refId": "A"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"fieldConfig": {
|
|
98
|
+
"defaults": {
|
|
99
|
+
"unit": "reqps",
|
|
100
|
+
"custom": {
|
|
101
|
+
"drawStyle": "line",
|
|
102
|
+
"lineInterpolation": "smooth",
|
|
103
|
+
"fillOpacity": 20,
|
|
104
|
+
"pointSize": 5,
|
|
105
|
+
"showPoints": "auto",
|
|
106
|
+
"stacking": { "mode": "none" }
|
|
107
|
+
},
|
|
108
|
+
"color": { "mode": "palette-classic" }
|
|
109
|
+
},
|
|
110
|
+
"overrides": []
|
|
111
|
+
},
|
|
112
|
+
"options": {
|
|
113
|
+
"legend": {
|
|
114
|
+
"displayMode": "table",
|
|
115
|
+
"placement": "right",
|
|
116
|
+
"calcs": ["mean", "max", "sum"]
|
|
117
|
+
},
|
|
118
|
+
"tooltip": { "mode": "multi" }
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"title": "Cost per LLM Model (via sampling)",
|
|
123
|
+
"description": "LLM cost breakdown — visible when MCP tools call LLMs internally",
|
|
124
|
+
"type": "piechart",
|
|
125
|
+
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 16 },
|
|
126
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
127
|
+
"targets": [
|
|
128
|
+
{
|
|
129
|
+
"expr": "sum by (gen_ai_request_model) (increase(gen_ai_client_request_cost_USD_sum[1h]))",
|
|
130
|
+
"legendFormat": "{{gen_ai_request_model}}",
|
|
131
|
+
"refId": "A",
|
|
132
|
+
"instant": true
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"fieldConfig": {
|
|
136
|
+
"defaults": {
|
|
137
|
+
"unit": "currencyUSD",
|
|
138
|
+
"decimals": 4,
|
|
139
|
+
"color": { "mode": "palette-classic" }
|
|
140
|
+
},
|
|
141
|
+
"overrides": []
|
|
142
|
+
},
|
|
143
|
+
"options": {
|
|
144
|
+
"legend": {
|
|
145
|
+
"displayMode": "table",
|
|
146
|
+
"placement": "right",
|
|
147
|
+
"values": ["value", "percent"]
|
|
148
|
+
},
|
|
149
|
+
"pieType": "donut",
|
|
150
|
+
"tooltip": { "mode": "multi" }
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"title": "Cost Over Time",
|
|
155
|
+
"description": "LLM cost rate — attribute to tools via trace context",
|
|
156
|
+
"type": "timeseries",
|
|
157
|
+
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 16 },
|
|
158
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
159
|
+
"targets": [
|
|
160
|
+
{
|
|
161
|
+
"expr": "sum by (gen_ai_request_model) (rate(gen_ai_client_request_cost_USD_sum[5m])) * 3600",
|
|
162
|
+
"legendFormat": "{{gen_ai_request_model}} ($/hr)",
|
|
163
|
+
"refId": "A"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"fieldConfig": {
|
|
167
|
+
"defaults": {
|
|
168
|
+
"unit": "currencyUSD",
|
|
169
|
+
"decimals": 4,
|
|
170
|
+
"custom": {
|
|
171
|
+
"drawStyle": "line",
|
|
172
|
+
"lineInterpolation": "smooth",
|
|
173
|
+
"fillOpacity": 15,
|
|
174
|
+
"pointSize": 5,
|
|
175
|
+
"showPoints": "auto"
|
|
176
|
+
},
|
|
177
|
+
"color": { "mode": "palette-classic" }
|
|
178
|
+
},
|
|
179
|
+
"overrides": []
|
|
180
|
+
},
|
|
181
|
+
"options": {
|
|
182
|
+
"legend": {
|
|
183
|
+
"displayMode": "table",
|
|
184
|
+
"placement": "bottom",
|
|
185
|
+
"calcs": ["sum", "mean"]
|
|
186
|
+
},
|
|
187
|
+
"tooltip": { "mode": "multi" }
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"title": "Tool Performance Summary",
|
|
192
|
+
"description": "All tools: call count, avg duration, error rate, unique callers",
|
|
193
|
+
"type": "table",
|
|
194
|
+
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
|
|
195
|
+
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
196
|
+
"targets": [
|
|
197
|
+
{
|
|
198
|
+
"expr": "sum by (gen_ai_tool_name) (increase(gen_ai_mcp_tool_calls_total[1h]))",
|
|
199
|
+
"legendFormat": "",
|
|
200
|
+
"refId": "calls",
|
|
201
|
+
"format": "table",
|
|
202
|
+
"instant": true
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"expr": "sum by (gen_ai_tool_name) (rate(gen_ai_mcp_tool_duration_sum[1h])) / sum by (gen_ai_tool_name) (rate(gen_ai_mcp_tool_duration_count[1h]))",
|
|
206
|
+
"legendFormat": "",
|
|
207
|
+
"refId": "avg_duration",
|
|
208
|
+
"format": "table",
|
|
209
|
+
"instant": true
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"expr": "sum by (gen_ai_tool_name) (increase(gen_ai_mcp_tool_errors_total[1h])) / sum by (gen_ai_tool_name) (increase(gen_ai_mcp_tool_calls_total[1h]))",
|
|
213
|
+
"legendFormat": "",
|
|
214
|
+
"refId": "error_rate",
|
|
215
|
+
"format": "table",
|
|
216
|
+
"instant": true
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"expr": "count by (gen_ai_tool_name) (sum by (gen_ai_tool_name, mcp_session_id) (gen_ai_mcp_tool_callers_total))",
|
|
220
|
+
"legendFormat": "",
|
|
221
|
+
"refId": "callers",
|
|
222
|
+
"format": "table",
|
|
223
|
+
"instant": true
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"transformations": [
|
|
227
|
+
{ "id": "merge" },
|
|
228
|
+
{
|
|
229
|
+
"id": "organize",
|
|
230
|
+
"options": {
|
|
231
|
+
"excludeByName": { "Time": true },
|
|
232
|
+
"renameByName": {
|
|
233
|
+
"gen_ai_tool_name": "Tool",
|
|
234
|
+
"Value #calls": "Calls (1h)",
|
|
235
|
+
"Value #avg_duration": "Avg Duration (ms)",
|
|
236
|
+
"Value #error_rate": "Error Rate",
|
|
237
|
+
"Value #callers": "Unique Callers"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
"fieldConfig": {
|
|
243
|
+
"defaults": {
|
|
244
|
+
"custom": { "align": "center" }
|
|
245
|
+
},
|
|
246
|
+
"overrides": [
|
|
247
|
+
{
|
|
248
|
+
"matcher": { "id": "byName", "options": "Avg Duration (ms)" },
|
|
249
|
+
"properties": [
|
|
250
|
+
{ "id": "unit", "value": "ms" },
|
|
251
|
+
{ "id": "decimals", "value": 1 }
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"matcher": { "id": "byName", "options": "Error Rate" },
|
|
256
|
+
"properties": [
|
|
257
|
+
{ "id": "unit", "value": "percentunit" },
|
|
258
|
+
{ "id": "decimals", "value": 2 },
|
|
259
|
+
{
|
|
260
|
+
"id": "thresholds",
|
|
261
|
+
"value": {
|
|
262
|
+
"mode": "absolute",
|
|
263
|
+
"steps": [
|
|
264
|
+
{ "color": "green", "value": null },
|
|
265
|
+
{ "color": "yellow", "value": 0.05 },
|
|
266
|
+
{ "color": "red", "value": 0.1 }
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{ "id": "custom.displayMode", "value": "color-background" }
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"matcher": { "id": "byName", "options": "Calls (1h)" },
|
|
275
|
+
"properties": [{ "id": "decimals", "value": 0 }]
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"schemaVersion": 39,
|
|
282
|
+
"tags": ["toad-eye", "mcp", "analytics", "cost"],
|
|
283
|
+
"templating": {
|
|
284
|
+
"list": []
|
|
285
|
+
},
|
|
286
|
+
"time": {
|
|
287
|
+
"from": "now-1h",
|
|
288
|
+
"to": "now"
|
|
289
|
+
},
|
|
290
|
+
"timepicker": {},
|
|
291
|
+
"timezone": "browser",
|
|
292
|
+
"title": "MCP Tool Analytics",
|
|
293
|
+
"uid": "toad-eye-mcp-analytics",
|
|
294
|
+
"version": 1
|
|
295
|
+
}
|