toad-eye 2.8.0 → 2.9.1
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 +1 -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 +13 -3
- package/dist/index.d.ts +1 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -169,4 +169,4 @@ Full metrics and span attribute reference: [COMPATIBILITY.md](https://github.com
|
|
|
169
169
|
|
|
170
170
|
## Tech Stack
|
|
171
171
|
|
|
172
|
-
TypeScript · OpenTelemetry SDK 2.x · Hono · Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector) · Vitest (
|
|
172
|
+
TypeScript · OpenTelemetry SDK 2.x · Hono · Docker Compose (Prometheus, Jaeger, Grafana, OTel Collector) · Vitest (278+ tests)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpanProcessor that calls user's onSpanEnd callback with structured span data.
|
|
3
|
+
*
|
|
4
|
+
* Extracts LLM/MCP-relevant attributes from ReadableSpan into a clean
|
|
5
|
+
* SpanEndData object — user never touches OTel SDK internals.
|
|
6
|
+
*/
|
|
7
|
+
import type { SpanProcessor, ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
8
|
+
/** Structured span data passed to the onSpanEnd callback. */
|
|
9
|
+
export interface SpanEndData {
|
|
10
|
+
readonly traceId: string;
|
|
11
|
+
readonly spanId: string;
|
|
12
|
+
readonly parentSpanId: string | undefined;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly kind: "client" | "server" | "internal" | "producer" | "consumer";
|
|
15
|
+
readonly status: "ok" | "error" | "unset";
|
|
16
|
+
readonly durationMs: number;
|
|
17
|
+
readonly startTime: Date;
|
|
18
|
+
readonly endTime: Date;
|
|
19
|
+
readonly attributes: Readonly<Record<string, string | number | boolean>>;
|
|
20
|
+
readonly error: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
export type OnSpanEndCallback = (data: SpanEndData) => void | Promise<void>;
|
|
23
|
+
export declare class ToadEyeSpanEndProcessor implements SpanProcessor {
|
|
24
|
+
private readonly callback;
|
|
25
|
+
constructor(callback: OnSpanEndCallback);
|
|
26
|
+
onStart(): void;
|
|
27
|
+
onEnd(span: ReadableSpan): void;
|
|
28
|
+
forceFlush(): Promise<void>;
|
|
29
|
+
shutdown(): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpanProcessor that calls user's onSpanEnd callback with structured span data.
|
|
3
|
+
*
|
|
4
|
+
* Extracts LLM/MCP-relevant attributes from ReadableSpan into a clean
|
|
5
|
+
* SpanEndData object — user never touches OTel SDK internals.
|
|
6
|
+
*/
|
|
7
|
+
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
8
|
+
const SPAN_KIND_MAP = {
|
|
9
|
+
[SpanKind.CLIENT]: "client",
|
|
10
|
+
[SpanKind.SERVER]: "server",
|
|
11
|
+
[SpanKind.INTERNAL]: "internal",
|
|
12
|
+
[SpanKind.PRODUCER]: "producer",
|
|
13
|
+
[SpanKind.CONSUMER]: "consumer",
|
|
14
|
+
};
|
|
15
|
+
function hrtimeToDate(hrtime) {
|
|
16
|
+
return new Date(hrtime[0] * 1000 + hrtime[1] / 1_000_000);
|
|
17
|
+
}
|
|
18
|
+
function hrtimeDurationMs(start, end) {
|
|
19
|
+
return (end[0] - start[0]) * 1000 + (end[1] - start[1]) / 1_000_000;
|
|
20
|
+
}
|
|
21
|
+
function flattenAttributes(attrs) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
24
|
+
if (typeof value === "string" ||
|
|
25
|
+
typeof value === "number" ||
|
|
26
|
+
typeof value === "boolean") {
|
|
27
|
+
result[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function toSpanEndData(span) {
|
|
33
|
+
const ctx = span.spanContext();
|
|
34
|
+
return {
|
|
35
|
+
traceId: ctx.traceId,
|
|
36
|
+
spanId: ctx.spanId,
|
|
37
|
+
parentSpanId: span.parentSpanId || undefined,
|
|
38
|
+
name: span.name,
|
|
39
|
+
kind: SPAN_KIND_MAP[span.kind] ?? "internal",
|
|
40
|
+
status: span.status.code === SpanStatusCode.ERROR
|
|
41
|
+
? "error"
|
|
42
|
+
: span.status.code === SpanStatusCode.OK
|
|
43
|
+
? "ok"
|
|
44
|
+
: "unset",
|
|
45
|
+
durationMs: hrtimeDurationMs(span.startTime, span.endTime),
|
|
46
|
+
startTime: hrtimeToDate(span.startTime),
|
|
47
|
+
endTime: hrtimeToDate(span.endTime),
|
|
48
|
+
attributes: flattenAttributes(span.attributes),
|
|
49
|
+
error: span.status.message || undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export class ToadEyeSpanEndProcessor {
|
|
53
|
+
callback;
|
|
54
|
+
constructor(callback) {
|
|
55
|
+
this.callback = callback;
|
|
56
|
+
}
|
|
57
|
+
onStart() { }
|
|
58
|
+
onEnd(span) {
|
|
59
|
+
try {
|
|
60
|
+
const result = this.callback(toSpanEndData(span));
|
|
61
|
+
// If callback returns a promise, don't block — fire and forget
|
|
62
|
+
if (result instanceof Promise) {
|
|
63
|
+
result.catch((err) => {
|
|
64
|
+
console.warn(`toad-eye: onSpanEnd callback failed: ${err instanceof Error ? err.message : err}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn(`toad-eye: onSpanEnd callback failed: ${err instanceof Error ? err.message : err}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
forceFlush() {
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
shutdown() {
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/core/tracer.js
CHANGED
|
@@ -13,6 +13,7 @@ import { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from
|
|
|
13
13
|
import { resetMcpMetrics } from "../mcp/metrics.js";
|
|
14
14
|
import { BudgetTracker } from "../budget/index.js";
|
|
15
15
|
import { ToadEyeAISpanProcessor } from "../vercel.js";
|
|
16
|
+
import { ToadEyeSpanEndProcessor } from "./span-end-processor.js";
|
|
16
17
|
const DEFAULT_ENDPOINT = "http://localhost:4318";
|
|
17
18
|
const DEFAULT_CLOUD_ENDPOINT = "https://cloud.toad-eye.dev";
|
|
18
19
|
let sdk = null;
|
|
@@ -81,11 +82,20 @@ export function initObservability(config) {
|
|
|
81
82
|
exporter: metricExporter,
|
|
82
83
|
exportIntervalMillis: isCloudMode ? 10_000 : 5_000,
|
|
83
84
|
});
|
|
84
|
-
// When custom SpanProcessors are needed (e.g., Vercel AI SDK),
|
|
85
|
+
// When custom SpanProcessors are needed (e.g., Vercel AI SDK, onSpanEnd),
|
|
85
86
|
// we must also include a BatchSpanProcessor for trace export —
|
|
86
87
|
// NodeSDK won't create one automatically when spanProcessors is provided.
|
|
87
|
-
const
|
|
88
|
-
|
|
88
|
+
const needsCustomProcessors = config.instrument?.includes("ai") || config.onSpanEnd !== undefined;
|
|
89
|
+
const spanProcessors = needsCustomProcessors
|
|
90
|
+
? [
|
|
91
|
+
new BatchSpanProcessor(traceExporter),
|
|
92
|
+
...(config.instrument?.includes("ai")
|
|
93
|
+
? [new ToadEyeAISpanProcessor()]
|
|
94
|
+
: []),
|
|
95
|
+
...(config.onSpanEnd !== undefined
|
|
96
|
+
? [new ToadEyeSpanEndProcessor(config.onSpanEnd)]
|
|
97
|
+
: []),
|
|
98
|
+
]
|
|
89
99
|
: [];
|
|
90
100
|
// SDK-side head sampling (default: 1.0 = send everything to Collector)
|
|
91
101
|
const sdkRate = config.sampling?.sdkRate ?? 1.0;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,3 +20,4 @@ export { LLM_METRICS } from "./types/index.js";
|
|
|
20
20
|
/** @deprecated Use GEN_AI_ATTRS instead */
|
|
21
21
|
export { LLM_ATTRS } from "./types/index.js";
|
|
22
22
|
export { ToadEyeAISpanProcessor, withToadEye } from "./vercel.js";
|
|
23
|
+
export type { SpanEndData, OnSpanEndCallback, } from "./core/span-end-processor.js";
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InstrumentTarget } from "./providers.js";
|
|
2
2
|
import type { BudgetConfig, BudgetExceededMode, DowngradeCallback } from "../budget/types.js";
|
|
3
|
+
import type { OnSpanEndCallback } from "../core/span-end-processor.js";
|
|
3
4
|
/** Sampling configuration. Tail sampling runs in OTel Collector, SDK sends all spans. */
|
|
4
5
|
export interface SamplingConfig {
|
|
5
6
|
/** SDK-side sampling rate (0.0–1.0). Default: 1.0 (send everything to Collector). */
|
|
@@ -60,4 +61,6 @@ export interface ToadEyeConfig {
|
|
|
60
61
|
readonly sessionId?: string | undefined;
|
|
61
62
|
/** Dynamic session ID extractor — called per traceLLMCall to resolve session ID. */
|
|
62
63
|
readonly sessionExtractor?: (() => string | undefined) | undefined;
|
|
64
|
+
/** Callback invoked when any span ends. Receives structured span data for eval pipelines. */
|
|
65
|
+
readonly onSpanEnd?: OnSpanEndCallback | undefined;
|
|
63
66
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export declare const INSTRUMENTATION_NAME = "toad-eye";
|
|
12
12
|
export type { LLMProvider, InstrumentTarget } from "./providers.js";
|
|
13
13
|
export type { ToadEyeConfig, SamplingConfig } from "./config.js";
|
|
14
|
+
export type { SpanEndData, OnSpanEndCallback, } from "../core/span-end-processor.js";
|
|
14
15
|
export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
|
|
15
16
|
export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
|
|
16
17
|
export type { MetricName } from "./metrics.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toad-eye",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"build": "tsc -p tsconfig.build.json",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
|
-
"prepublishOnly": "
|
|
52
|
+
"prepublishOnly": "grep -q 'toad-eye' README.md || { echo '❌ README.md is missing or broken'; exit 1; } && npm run build"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|
|
55
55
|
"type": "git",
|