toad-eye 2.2.0 → 2.3.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.
Files changed (47) hide show
  1. package/README.md +166 -152
  2. package/dist/agent.d.ts +19 -28
  3. package/dist/agent.js +46 -27
  4. package/dist/alerts/channels.js +24 -6
  5. package/dist/alerts/index.js +46 -2
  6. package/dist/alerts/manager.d.ts +1 -0
  7. package/dist/alerts/manager.js +36 -21
  8. package/dist/budget/tracker.d.ts +16 -6
  9. package/dist/budget/tracker.js +38 -17
  10. package/dist/budget/types.d.ts +2 -0
  11. package/dist/cli.d.ts +8 -1
  12. package/dist/cli.js +106 -54
  13. package/dist/core/metrics.d.ts +4 -0
  14. package/dist/core/metrics.js +19 -0
  15. package/dist/core/pricing.d.ts +2 -0
  16. package/dist/core/pricing.js +4 -0
  17. package/dist/core/spans.d.ts +5 -3
  18. package/dist/core/spans.js +79 -20
  19. package/dist/core/tracer.d.ts +0 -3
  20. package/dist/core/tracer.js +40 -7
  21. package/dist/drift/baseline.js +7 -1
  22. package/dist/drift/monitor.js +6 -6
  23. package/dist/index.d.ts +7 -2
  24. package/dist/index.js +6 -1
  25. package/dist/instrumentations/anthropic.d.ts +1 -1
  26. package/dist/instrumentations/anthropic.js +2 -3
  27. package/dist/instrumentations/create.js +80 -19
  28. package/dist/instrumentations/gemini.d.ts +1 -1
  29. package/dist/instrumentations/gemini.js +6 -7
  30. package/dist/instrumentations/openai.d.ts +1 -1
  31. package/dist/instrumentations/openai.js +2 -3
  32. package/dist/instrumentations/registry.d.ts +3 -1
  33. package/dist/instrumentations/registry.js +28 -5
  34. package/dist/instrumentations/types.d.ts +4 -2
  35. package/dist/types/attributes.d.ts +3 -0
  36. package/dist/types/attributes.js +3 -0
  37. package/dist/types/config.d.ts +22 -2
  38. package/dist/types/index.d.ts +3 -3
  39. package/dist/types/metrics.d.ts +2 -0
  40. package/dist/types/metrics.js +3 -0
  41. package/dist/types/providers.d.ts +6 -1
  42. package/dist/types/spans.d.ts +11 -2
  43. package/dist/vercel.d.ts +53 -0
  44. package/dist/vercel.js +106 -0
  45. package/package.json +29 -3
  46. package/templates/grafana/dashboards/agent-workflow.json +207 -0
  47. package/templates/otel-collector.yml +25 -1
@@ -1,25 +1,48 @@
1
- import { diag } from "@opentelemetry/api";
2
1
  const instrumentations = new Map();
3
2
  const active = new Set();
4
3
  export function register(inst) {
5
4
  instrumentations.set(inst.name, inst);
6
5
  }
7
- export function enableAll(providers) {
6
+ /**
7
+ * Lazy registration — loads provider modules and registers instrumentations
8
+ * only when enableAll() is first called. This avoids pulling in create.ts,
9
+ * metrics.ts, and node:module on every `import` of toad-eye.
10
+ */
11
+ let registered = false;
12
+ /** Skip lazy registration (used in tests that register mocks manually). */
13
+ export function skipAutoRegister() {
14
+ registered = true;
15
+ }
16
+ async function ensureRegistered() {
17
+ if (registered)
18
+ return;
19
+ registered = true;
20
+ const [openai, anthropic, gemini] = await Promise.all([
21
+ import("./openai.js"),
22
+ import("./anthropic.js"),
23
+ import("./gemini.js"),
24
+ ]);
25
+ register(openai.openaiInstrumentation);
26
+ register(anthropic.anthropicInstrumentation);
27
+ register(gemini.geminiInstrumentation);
28
+ }
29
+ export async function enableAll(providers) {
30
+ await ensureRegistered();
31
+ const validProviders = Array.from(instrumentations.keys()).join(", ");
8
32
  for (const name of providers) {
9
33
  if (active.has(name))
10
34
  continue;
11
35
  const inst = instrumentations.get(name);
12
36
  if (!inst) {
13
- diag.warn(`toad-eye: unknown provider "${name}", skipping`);
37
+ console.warn(`toad-eye: unknown provider "${name}" — valid providers: ${validProviders}`);
14
38
  continue;
15
39
  }
16
40
  const patched = inst.enable();
17
41
  if (patched) {
18
42
  active.add(name);
19
- diag.debug(`toad-eye: auto-instrumented ${name}`);
20
43
  }
21
44
  else {
22
- diag.debug(`toad-eye: ${name} SDK not found, skipping`);
45
+ console.warn(`toad-eye: "${name}" SDK not found — install it to enable auto-instrumentation: npm install ${name === "gemini" ? "@google/generative-ai" : name}`);
23
46
  }
24
47
  }
25
48
  }
@@ -18,8 +18,10 @@ export interface PatchTarget {
18
18
  getPrototype: (sdk: any) => any | undefined;
19
19
  /** Method name on the prototype to patch */
20
20
  method: string;
21
- /** Extract LLMCallInput fields from the request arguments */
22
- extractRequest: (body: unknown) => {
21
+ /** Extract LLMCallInput fields from the request arguments.
22
+ * thisArg is the SDK object instance — use it when the model name is not in the request body
23
+ * (e.g., Gemini's GenerativeModel stores it as this.model). */
24
+ extractRequest: (body: unknown, thisArg?: unknown) => {
23
25
  prompt: string;
24
26
  model: string;
25
27
  temperature?: number;
@@ -19,6 +19,9 @@ export declare const GEN_AI_ATTRS: {
19
19
  readonly AGENT_STEP_NUMBER: "gen_ai.agent.step.number";
20
20
  readonly AGENT_TOOL_NAME: "gen_ai.agent.tool.name";
21
21
  readonly AGENT_STEP_CONTENT: "gen_ai.agent.step.content";
22
+ readonly AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to";
23
+ readonly AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason";
24
+ readonly AGENT_LOOP_COUNT: "gen_ai.agent.loop_count";
22
25
  readonly GUARD_MODE: "gen_ai.toad_eye.guard.mode";
23
26
  readonly GUARD_PASSED: "gen_ai.toad_eye.guard.passed";
24
27
  readonly GUARD_FAILURE_REASON: "gen_ai.toad_eye.guard.failure_reason";
@@ -21,6 +21,9 @@ export const GEN_AI_ATTRS = {
21
21
  AGENT_STEP_NUMBER: "gen_ai.agent.step.number",
22
22
  AGENT_TOOL_NAME: "gen_ai.agent.tool.name",
23
23
  AGENT_STEP_CONTENT: "gen_ai.agent.step.content",
24
+ AGENT_HANDOFF_TO: "gen_ai.agent.handoff.to",
25
+ AGENT_HANDOFF_REASON: "gen_ai.agent.handoff.reason",
26
+ AGENT_LOOP_COUNT: "gen_ai.agent.loop_count",
24
27
  // Guard (shadow mode) attributes
25
28
  GUARD_MODE: "gen_ai.toad_eye.guard.mode",
26
29
  GUARD_PASSED: "gen_ai.toad_eye.guard.passed",
@@ -1,5 +1,19 @@
1
- import type { LLMProvider } from "./providers.js";
1
+ import type { InstrumentTarget } from "./providers.js";
2
2
  import type { BudgetConfig, BudgetExceededMode, DowngradeCallback } from "../budget/types.js";
3
+ /** Sampling configuration. Tail sampling runs in OTel Collector, SDK sends all spans. */
4
+ export interface SamplingConfig {
5
+ /** SDK-side sampling rate (0.0–1.0). Default: 1.0 (send everything to Collector). */
6
+ readonly sdkRate?: number | undefined;
7
+ /** Collector-side tail sampling settings (used when generating otel-collector.yml). */
8
+ readonly collector?: {
9
+ /** Keep 100% of error traces. Default: true */
10
+ readonly keepErrors?: boolean | undefined;
11
+ /** Keep 100% of traces slower than this (ms). Default: 2000 */
12
+ readonly highLatencyMs?: number | undefined;
13
+ /** Sample this % of healthy traffic (0–100). Default: 10 */
14
+ readonly healthyRate?: number | undefined;
15
+ } | undefined;
16
+ }
3
17
  /**
4
18
  * Configuration for initObservability().
5
19
  * This is what the user passes when connecting toad-eye to their service.
@@ -20,7 +34,13 @@ export interface ToadEyeConfig {
20
34
  readonly salt?: string | undefined;
21
35
  /** Regex patterns to redact from prompt/completion text before recording. */
22
36
  readonly redactPatterns?: readonly RegExp[] | undefined;
23
- readonly instrument?: readonly LLMProvider[] | undefined;
37
+ /** Enable built-in redaction patterns for common PII: email, SSN, credit card, phone. */
38
+ readonly redactDefaults?: boolean | undefined;
39
+ /** Log what was masked to console (for debugging redaction config). No data is sent externally. */
40
+ readonly auditMasking?: boolean | undefined;
41
+ /** Configure trace sampling. Tail sampling happens in OTel Collector, not SDK. */
42
+ readonly sampling?: SamplingConfig | undefined;
43
+ readonly instrument?: readonly InstrumentTarget[] | undefined;
24
44
  /** Budget limits — daily, per-user, per-model spend caps in USD. */
25
45
  readonly budgets?: BudgetConfig | undefined;
26
46
  /** What to do when budget is exceeded: 'warn' (log), 'block' (throw), 'downgrade' (callback). */
@@ -9,9 +9,9 @@
9
9
  * - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
10
10
  */
11
11
  export declare const INSTRUMENTATION_NAME = "toad-eye";
12
- export type { LLMProvider } from "./providers.js";
13
- export type { ToadEyeConfig } from "./config.js";
12
+ export type { LLMProvider, InstrumentTarget } from "./providers.js";
13
+ export type { ToadEyeConfig, SamplingConfig } from "./config.js";
14
14
  export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
15
15
  export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
16
16
  export type { MetricName } from "./metrics.js";
17
- export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, GuardMode, GuardResult, } from "./spans.js";
17
+ export type { SpanStatus, LLMSpanAttributes, AgentStepType, AgentStepInput, AgentQueryOptions, GuardMode, GuardResult, } from "./spans.js";
@@ -21,6 +21,8 @@ export declare const GEN_AI_METRICS: {
21
21
  readonly BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded";
22
22
  readonly BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked";
23
23
  readonly BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded";
24
+ readonly RESPONSE_EMPTY: "gen_ai.toad_eye.response.empty";
25
+ readonly RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token";
24
26
  };
25
27
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
26
28
  export declare const LLM_METRICS: {
@@ -26,6 +26,9 @@ export const GEN_AI_METRICS = {
26
26
  BUDGET_EXCEEDED: "gen_ai.toad_eye.budget.exceeded",
27
27
  BUDGET_BLOCKED: "gen_ai.toad_eye.budget.blocked",
28
28
  BUDGET_DOWNGRADED: "gen_ai.toad_eye.budget.downgraded",
29
+ // Response quality proxy metrics
30
+ RESPONSE_EMPTY: "gen_ai.toad_eye.response.empty",
31
+ RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token",
29
32
  };
30
33
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
31
34
  export const LLM_METRICS = {
@@ -2,4 +2,9 @@
2
2
  * LLM providers supported by toad-eye.
3
3
  * Values follow OTel GenAI semconv `gen_ai.provider.name`.
4
4
  */
5
- export type LLMProvider = "anthropic" | "gemini" | "openai";
5
+ export type LLMProvider = "anthropic" | "gemini" | "openai" | (string & {});
6
+ /**
7
+ * All instrumentable targets — includes LLM providers + framework SDKs.
8
+ * 'ai' = Vercel AI SDK (uses SpanProcessor, not monkey-patching).
9
+ */
10
+ export type InstrumentTarget = LLMProvider | "ai";
@@ -1,14 +1,23 @@
1
1
  import type { LLMProvider } from "./providers.js";
2
2
  /** Span status values used in trace attributes. */
3
3
  export type SpanStatus = "success" | "error";
4
- /** ReAct agent step types: think → act → observe → answer */
5
- export type AgentStepType = "think" | "act" | "observe" | "answer";
4
+ /** ReAct agent step types: think → act → observe → answer → handoff */
5
+ export type AgentStepType = "think" | "act" | "observe" | "answer" | "handoff";
6
6
  /** Input for traceAgentStep — describes a single agent step */
7
7
  export interface AgentStepInput {
8
8
  readonly type: AgentStepType;
9
9
  readonly stepNumber: number;
10
10
  readonly content?: string | undefined;
11
11
  readonly toolName?: string | undefined;
12
+ /** Target agent name for handoff steps */
13
+ readonly toAgent?: string | undefined;
14
+ /** Reason for handoff */
15
+ readonly handoffReason?: string | undefined;
16
+ }
17
+ /** Options for traceAgentQuery */
18
+ export interface AgentQueryOptions {
19
+ /** Max steps before recording a warning. Default: 25 */
20
+ readonly maxSteps?: number | undefined;
12
21
  }
13
22
  /** Guard execution mode */
14
23
  export type GuardMode = "shadow" | "enforce";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Vercel AI SDK integration — enriches AI SDK OTel spans with toad-eye
3
+ * cost tracking and metrics. No monkey-patching needed.
4
+ *
5
+ * The Vercel AI SDK already emits OTel spans when `experimental_telemetry`
6
+ * is enabled. This module adds a SpanProcessor that intercepts those spans
7
+ * and calculates cost from token usage.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { initObservability } from 'toad-eye';
12
+ * import { withToadEye } from 'toad-eye/vercel';
13
+ *
14
+ * initObservability({ serviceName: 'my-app' });
15
+ *
16
+ * const result = await generateText({
17
+ * model: openai('gpt-4o'),
18
+ * prompt: 'Hello',
19
+ * experimental_telemetry: withToadEye(),
20
+ * });
21
+ * ```
22
+ */
23
+ import type { SpanProcessor, ReadableSpan } from "@opentelemetry/sdk-trace-base";
24
+ /**
25
+ * OTel SpanProcessor that enriches Vercel AI SDK spans with toad-eye
26
+ * cost attributes and records metrics.
27
+ */
28
+ export declare class ToadEyeAISpanProcessor implements SpanProcessor {
29
+ forceFlush(): Promise<void>;
30
+ shutdown(): Promise<void>;
31
+ onStart(): void;
32
+ onEnd(span: ReadableSpan): void;
33
+ }
34
+ /**
35
+ * Helper for `experimental_telemetry` option — enables telemetry with
36
+ * optional toad-eye metadata.
37
+ *
38
+ * ```ts
39
+ * const result = await generateText({
40
+ * model: openai('gpt-4o'),
41
+ * prompt: 'Hello',
42
+ * experimental_telemetry: withToadEye({ functionId: 'my-feature' }),
43
+ * });
44
+ * ```
45
+ */
46
+ export declare function withToadEye(options?: {
47
+ functionId?: string;
48
+ metadata?: Record<string, string>;
49
+ }): {
50
+ functionId?: string;
51
+ metadata?: Record<string, string>;
52
+ isEnabled: true;
53
+ };
package/dist/vercel.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Vercel AI SDK integration — enriches AI SDK OTel spans with toad-eye
3
+ * cost tracking and metrics. No monkey-patching needed.
4
+ *
5
+ * The Vercel AI SDK already emits OTel spans when `experimental_telemetry`
6
+ * is enabled. This module adds a SpanProcessor that intercepts those spans
7
+ * and calculates cost from token usage.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { initObservability } from 'toad-eye';
12
+ * import { withToadEye } from 'toad-eye/vercel';
13
+ *
14
+ * initObservability({ serviceName: 'my-app' });
15
+ *
16
+ * const result = await generateText({
17
+ * model: openai('gpt-4o'),
18
+ * prompt: 'Hello',
19
+ * experimental_telemetry: withToadEye(),
20
+ * });
21
+ * ```
22
+ */
23
+ import { calculateCost } from "./core/pricing.js";
24
+ import { recordRequestCost, recordTokens, recordRequest, recordRequestDuration, } from "./core/metrics.js";
25
+ // Vercel AI SDK span operation names
26
+ const AI_SDK_OPERATIONS = new Set([
27
+ "ai.generateText",
28
+ "ai.streamText",
29
+ "ai.generateObject",
30
+ "ai.streamObject",
31
+ ]);
32
+ function isAiSdkSpan(span) {
33
+ const opId = span.attributes["ai.operationId"];
34
+ if (typeof opId === "string")
35
+ return AI_SDK_OPERATIONS.has(opId);
36
+ return AI_SDK_OPERATIONS.has(span.name);
37
+ }
38
+ function getStringAttr(span, key) {
39
+ const val = span.attributes[key];
40
+ return typeof val === "string" ? val : undefined;
41
+ }
42
+ function getNumberAttr(span, key) {
43
+ const val = span.attributes[key];
44
+ return typeof val === "number" ? val : 0;
45
+ }
46
+ /**
47
+ * OTel SpanProcessor that enriches Vercel AI SDK spans with toad-eye
48
+ * cost attributes and records metrics.
49
+ */
50
+ export class ToadEyeAISpanProcessor {
51
+ forceFlush() {
52
+ return Promise.resolve();
53
+ }
54
+ shutdown() {
55
+ return Promise.resolve();
56
+ }
57
+ onStart() {
58
+ // no-op — we process on end
59
+ }
60
+ onEnd(span) {
61
+ if (!isAiSdkSpan(span))
62
+ return;
63
+ const model = getStringAttr(span, "gen_ai.request.model") ??
64
+ getStringAttr(span, "ai.model.id") ??
65
+ "unknown";
66
+ const provider = getStringAttr(span, "gen_ai.system") ??
67
+ getStringAttr(span, "ai.model.provider")?.split(".")[0] ??
68
+ "unknown";
69
+ const inputTokens = getNumberAttr(span, "gen_ai.usage.input_tokens") ||
70
+ getNumberAttr(span, "ai.usage.promptTokens");
71
+ const outputTokens = getNumberAttr(span, "gen_ai.usage.output_tokens") ||
72
+ getNumberAttr(span, "ai.usage.completionTokens");
73
+ // Calculate cost from token usage
74
+ const cost = calculateCost(model, inputTokens, outputTokens);
75
+ // We can't modify a ReadableSpan's attributes directly, but we can
76
+ // record metrics which is the primary value-add
77
+ const durationMs = (span.endTime[0] - span.startTime[0]) * 1000 +
78
+ (span.endTime[1] - span.startTime[1]) / 1_000_000;
79
+ recordRequest(provider, model);
80
+ recordRequestDuration(durationMs, provider, model);
81
+ if (cost > 0) {
82
+ recordRequestCost(cost, provider, model);
83
+ }
84
+ if (inputTokens + outputTokens > 0) {
85
+ recordTokens(inputTokens + outputTokens, provider, model);
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Helper for `experimental_telemetry` option — enables telemetry with
91
+ * optional toad-eye metadata.
92
+ *
93
+ * ```ts
94
+ * const result = await generateText({
95
+ * model: openai('gpt-4o'),
96
+ * prompt: 'Hello',
97
+ * experimental_telemetry: withToadEye({ functionId: 'my-feature' }),
98
+ * });
99
+ * ```
100
+ */
101
+ export function withToadEye(options) {
102
+ return {
103
+ isEnabled: true,
104
+ ...options,
105
+ };
106
+ }
package/package.json CHANGED
@@ -1,10 +1,32 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Auto-instrumentation, cost tracking, budget guards, and observability for LLM services",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./alerts": {
14
+ "import": "./dist/alerts/index.js",
15
+ "types": "./dist/alerts/index.d.ts"
16
+ },
17
+ "./drift": {
18
+ "import": "./dist/drift/index.js",
19
+ "types": "./dist/drift/index.d.ts"
20
+ },
21
+ "./export": {
22
+ "import": "./dist/export.js",
23
+ "types": "./dist/export.d.ts"
24
+ },
25
+ "./vercel": {
26
+ "import": "./dist/vercel.js",
27
+ "types": "./dist/vercel.d.ts"
28
+ }
29
+ },
8
30
  "bin": {
9
31
  "toad-eye": "dist/cli.js"
10
32
  },
@@ -51,13 +73,15 @@
51
73
  "@opentelemetry/resources": "^2.6.0",
52
74
  "@opentelemetry/sdk-metrics": "^2.6.0",
53
75
  "@opentelemetry/sdk-node": "^0.213.0",
76
+ "@opentelemetry/sdk-trace-base": "^2.6.0",
77
+ "@opentelemetry/sdk-trace-node": "^2.6.0",
54
78
  "@opentelemetry/semantic-conventions": "^1.40.0",
55
- "nodemailer": "^8.0.3",
56
79
  "yaml": "^2.7.0"
57
80
  },
58
81
  "peerDependencies": {
59
82
  "@anthropic-ai/sdk": ">=0.30.0",
60
83
  "@google/generative-ai": ">=0.20.0",
84
+ "nodemailer": ">=8.0.0",
61
85
  "openai": ">=4.0.0"
62
86
  },
63
87
  "peerDependenciesMeta": {
@@ -69,11 +93,13 @@
69
93
  },
70
94
  "@google/generative-ai": {
71
95
  "optional": true
96
+ },
97
+ "nodemailer": {
98
+ "optional": true
72
99
  }
73
100
  },
74
101
  "devDependencies": {
75
102
  "@types/nodemailer": "^7.0.11",
76
- "playwright": "^1.58.2",
77
103
  "vitest": "^4.1.0"
78
104
  }
79
105
  }
@@ -0,0 +1,207 @@
1
+ {
2
+ "annotations": { "list": [] },
3
+ "editable": true,
4
+ "fiscalYearStartMonth": 0,
5
+ "graphTooltip": 1,
6
+ "id": null,
7
+ "links": [],
8
+ "panels": [
9
+ {
10
+ "title": "Avg Steps per Query",
11
+ "type": "stat",
12
+ "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
13
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
14
+ "targets": [
15
+ {
16
+ "expr": "sum(gen_ai_agent_steps_per_query_sum) / sum(gen_ai_agent_steps_per_query_count) or vector(0)",
17
+ "refId": "A"
18
+ }
19
+ ],
20
+ "fieldConfig": {
21
+ "defaults": {
22
+ "unit": "short",
23
+ "decimals": 1,
24
+ "thresholds": {
25
+ "steps": [
26
+ { "color": "green", "value": null },
27
+ { "color": "yellow", "value": 10 },
28
+ { "color": "red", "value": 20 }
29
+ ]
30
+ }
31
+ },
32
+ "overrides": []
33
+ }
34
+ },
35
+ {
36
+ "title": "Total Agent Queries",
37
+ "type": "stat",
38
+ "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
39
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
40
+ "targets": [
41
+ {
42
+ "expr": "sum(gen_ai_agent_steps_per_query_count)",
43
+ "refId": "A"
44
+ }
45
+ ],
46
+ "fieldConfig": {
47
+ "defaults": { "unit": "short" },
48
+ "overrides": []
49
+ }
50
+ },
51
+ {
52
+ "title": "Total Tool Invocations",
53
+ "type": "stat",
54
+ "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
55
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
56
+ "targets": [
57
+ {
58
+ "expr": "sum(gen_ai_agent_tool_usage_total)",
59
+ "refId": "A"
60
+ }
61
+ ],
62
+ "fieldConfig": {
63
+ "defaults": { "unit": "short" },
64
+ "overrides": []
65
+ }
66
+ },
67
+ {
68
+ "title": "Unique Tools Used",
69
+ "type": "stat",
70
+ "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
71
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
72
+ "targets": [
73
+ {
74
+ "expr": "count(count by (gen_ai_agent_tool_name) (gen_ai_agent_tool_usage_total))",
75
+ "refId": "A"
76
+ }
77
+ ],
78
+ "fieldConfig": {
79
+ "defaults": { "unit": "short" },
80
+ "overrides": []
81
+ }
82
+ },
83
+ {
84
+ "title": "Agent Step Type Breakdown",
85
+ "description": "Distribution of think / act / observe / answer steps",
86
+ "type": "piechart",
87
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
88
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
89
+ "targets": [
90
+ {
91
+ "expr": "sum by (gen_ai_agent_step_type) (gen_ai_agent_steps_per_query_sum)",
92
+ "legendFormat": "{{gen_ai_agent_step_type}}",
93
+ "refId": "A"
94
+ }
95
+ ],
96
+ "options": {
97
+ "legend": {
98
+ "displayMode": "table",
99
+ "placement": "right",
100
+ "values": ["value", "percent"]
101
+ },
102
+ "pieType": "donut"
103
+ }
104
+ },
105
+ {
106
+ "title": "Tool Usage Frequency",
107
+ "description": "Invocations per tool name",
108
+ "type": "barchart",
109
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
110
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
111
+ "targets": [
112
+ {
113
+ "expr": "sort_desc(sum by (gen_ai_agent_tool_name) (gen_ai_agent_tool_usage_total))",
114
+ "legendFormat": "{{gen_ai_agent_tool_name}}",
115
+ "refId": "A"
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "title": "Steps per Query Over Time",
121
+ "type": "timeseries",
122
+ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 12 },
123
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
124
+ "targets": [
125
+ {
126
+ "expr": "sum(rate(gen_ai_agent_steps_per_query_sum[5m])) / sum(rate(gen_ai_agent_steps_per_query_count[5m]))",
127
+ "legendFormat": "avg steps/query",
128
+ "refId": "avg"
129
+ },
130
+ {
131
+ "expr": "histogram_quantile(0.95, sum by (le) (rate(gen_ai_agent_steps_per_query_bucket[5m])))",
132
+ "legendFormat": "p95 steps/query",
133
+ "refId": "p95"
134
+ }
135
+ ],
136
+ "fieldConfig": {
137
+ "defaults": {
138
+ "unit": "short",
139
+ "custom": { "fillOpacity": 10 }
140
+ },
141
+ "overrides": []
142
+ }
143
+ },
144
+ {
145
+ "title": "Tool Invocations Over Time",
146
+ "type": "timeseries",
147
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 },
148
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
149
+ "targets": [
150
+ {
151
+ "expr": "sum by (gen_ai_agent_tool_name) (rate(gen_ai_agent_tool_usage_total[5m]))",
152
+ "legendFormat": "{{gen_ai_agent_tool_name}}",
153
+ "refId": "A"
154
+ }
155
+ ],
156
+ "fieldConfig": {
157
+ "defaults": {
158
+ "unit": "reqps",
159
+ "custom": { "fillOpacity": 20, "stacking": { "mode": "normal" } }
160
+ },
161
+ "overrides": []
162
+ }
163
+ },
164
+ {
165
+ "title": "Steps Distribution Histogram",
166
+ "description": "How many steps do agent queries typically take?",
167
+ "type": "histogram",
168
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 },
169
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
170
+ "targets": [
171
+ {
172
+ "expr": "sum by (le) (gen_ai_agent_steps_per_query_bucket)",
173
+ "format": "heatmap",
174
+ "refId": "A"
175
+ }
176
+ ],
177
+ "fieldConfig": {
178
+ "defaults": {
179
+ "custom": { "fillOpacity": 80 }
180
+ },
181
+ "overrides": []
182
+ }
183
+ }
184
+ ],
185
+ "schemaVersion": 39,
186
+ "tags": ["toad-eye", "agent"],
187
+ "templating": {
188
+ "list": [
189
+ {
190
+ "name": "tool",
191
+ "type": "query",
192
+ "datasource": { "type": "prometheus", "uid": "prometheus" },
193
+ "query": "label_values(gen_ai_agent_tool_usage_total, gen_ai_agent_tool_name)",
194
+ "includeAll": true,
195
+ "multi": true,
196
+ "current": { "text": "All", "value": "$__all" },
197
+ "regex": ""
198
+ }
199
+ ]
200
+ },
201
+ "time": { "from": "now-6h", "to": "now" },
202
+ "timepicker": {},
203
+ "timezone": "utc",
204
+ "title": "toad-eye / Agent Workflow",
205
+ "uid": "toad-eye-agent-workflow",
206
+ "version": 1
207
+ }
@@ -8,6 +8,30 @@ processors:
8
8
  batch:
9
9
  timeout: 1s
10
10
 
11
+ # Tail-based sampling: keeps 100% errors + slow traces, samples healthy traffic.
12
+ # Only applies to traces pipeline — metrics always get 100%.
13
+ # To disable: remove tail_sampling from traces pipeline processors.
14
+ tail_sampling:
15
+ decision_wait: 10s
16
+ num_traces: 100000
17
+ expected_new_traces_per_sec: 100
18
+ policies:
19
+ # Keep all traces with errors
20
+ - name: errors-always
21
+ type: status_code
22
+ status_code:
23
+ status_codes: [ERROR]
24
+ # Keep all slow traces (> 2s)
25
+ - name: slow-traces
26
+ type: latency
27
+ latency:
28
+ threshold_ms: 2000
29
+ # Sample 10% of remaining healthy traffic
30
+ - name: probabilistic-fallback
31
+ type: probabilistic
32
+ probabilistic:
33
+ sampling_percentage: 10
34
+
11
35
  exporters:
12
36
  otlphttp/jaeger:
13
37
  endpoint: http://jaeger:4318
@@ -21,7 +45,7 @@ service:
21
45
  pipelines:
22
46
  traces:
23
47
  receivers: [otlp]
24
- processors: [batch]
48
+ processors: [tail_sampling, batch]
25
49
  exporters: [otlphttp/jaeger]
26
50
  metrics:
27
51
  receivers: [otlp]