toad-eye 1.1.0 → 2.0.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 +132 -87
- package/dist/alerts/channels.d.ts +2 -0
- package/dist/alerts/channels.js +81 -0
- package/dist/alerts/conditions.d.ts +29 -0
- package/dist/alerts/conditions.js +147 -0
- package/dist/alerts/grafana.d.ts +5 -0
- package/dist/alerts/grafana.js +19 -0
- package/dist/alerts/index.d.ts +5 -0
- package/dist/alerts/index.js +12 -0
- package/dist/alerts/manager.d.ts +13 -0
- package/dist/alerts/manager.js +87 -0
- package/dist/alerts/types.d.ts +46 -0
- package/dist/alerts/types.js +1 -0
- package/dist/cli.js +72 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/instrumentations/anthropic.d.ts +1 -0
- package/dist/instrumentations/anthropic.js +54 -0
- package/dist/instrumentations/create.d.ts +11 -0
- package/dist/instrumentations/create.js +67 -0
- package/dist/instrumentations/gemini.d.ts +1 -0
- package/dist/instrumentations/gemini.js +48 -0
- package/dist/instrumentations/openai.d.ts +1 -0
- package/dist/instrumentations/openai.js +58 -0
- package/dist/instrumentations/registry.d.ts +5 -0
- package/dist/instrumentations/registry.js +31 -0
- package/dist/instrumentations/types.d.ts +29 -0
- package/dist/instrumentations/types.js +1 -0
- package/dist/metrics.js +31 -16
- package/dist/pricing.d.ts +24 -0
- package/dist/pricing.js +52 -0
- package/dist/spans.d.ts +1 -1
- package/dist/spans.js +46 -20
- package/dist/tracer.d.ts +4 -1
- package/dist/tracer.js +9 -0
- package/dist/types/attributes.d.ts +36 -0
- package/dist/types/attributes.js +38 -0
- package/dist/types/config.d.ts +20 -0
- package/dist/types/config.js +1 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.js +13 -0
- package/dist/types/metrics.d.ts +25 -0
- package/dist/types/metrics.js +23 -0
- package/dist/types/providers.d.ts +5 -0
- package/dist/types/providers.js +1 -0
- package/dist/types/spans.d.ts +19 -0
- package/dist/types/spans.js +1 -0
- package/dist/types.d.ts +60 -25
- package/dist/types.js +47 -21
- package/package.json +15 -4
- package/templates/alerts.example.yml +54 -0
- package/templates/docker-compose.yml +1 -2
- package/templates/grafana/dashboards/cost-breakdown.json +208 -0
- package/templates/grafana/dashboards/error-drilldown.json +188 -0
- package/templates/grafana/dashboards/latency-analysis.json +134 -0
- package/templates/grafana/dashboards/llm-overview.json +161 -30
- package/templates/grafana/dashboards/model-comparison.json +143 -0
package/dist/spans.js
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
-
import {
|
|
3
|
+
import { GEN_AI_ATTRS, INSTRUMENTATION_NAME } from "./types/index.js";
|
|
3
4
|
import { recordRequestDuration, recordRequestCost, recordTokens, recordRequest, recordError, } from "./metrics.js";
|
|
4
5
|
import { getConfig } from "./tracer.js";
|
|
5
6
|
const tracer = trace.getTracer(INSTRUMENTATION_NAME);
|
|
6
|
-
function
|
|
7
|
-
return
|
|
7
|
+
function sha256(text) {
|
|
8
|
+
return createHash("sha256").update(text).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function processContent(text) {
|
|
11
|
+
const config = getConfig();
|
|
12
|
+
if (config?.recordContent === false)
|
|
13
|
+
return undefined;
|
|
14
|
+
let processed = text;
|
|
15
|
+
if (config?.redactPatterns?.length) {
|
|
16
|
+
for (const pattern of config.redactPatterns) {
|
|
17
|
+
processed = processed.replace(pattern, "[REDACTED]");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (config?.hashContent) {
|
|
21
|
+
return `sha256:${sha256(processed)}`;
|
|
22
|
+
}
|
|
23
|
+
return processed;
|
|
24
|
+
}
|
|
25
|
+
function resolveSessionId() {
|
|
26
|
+
const config = getConfig();
|
|
27
|
+
return config?.sessionExtractor?.() ?? config?.sessionId;
|
|
8
28
|
}
|
|
9
29
|
function setBaseAttributes(span, input) {
|
|
30
|
+
const prompt = processContent(input.prompt);
|
|
31
|
+
const sessionId = resolveSessionId();
|
|
10
32
|
span.setAttributes({
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
|
|
33
|
+
[GEN_AI_ATTRS.PROVIDER]: input.provider,
|
|
34
|
+
[GEN_AI_ATTRS.REQUEST_MODEL]: input.model,
|
|
35
|
+
[GEN_AI_ATTRS.TEMPERATURE]: input.temperature ?? 1.0,
|
|
36
|
+
[GEN_AI_ATTRS.OPERATION]: "chat",
|
|
37
|
+
...(prompt !== undefined && { [GEN_AI_ATTRS.PROMPT]: prompt }),
|
|
38
|
+
...(sessionId !== undefined && { [GEN_AI_ATTRS.SESSION_ID]: sessionId }),
|
|
15
39
|
});
|
|
16
40
|
}
|
|
17
41
|
export async function traceLLMCall(input, fn) {
|
|
18
|
-
return tracer.startActiveSpan(`
|
|
42
|
+
return tracer.startActiveSpan(`gen_ai.${input.provider}.${input.model}`, async (span) => {
|
|
19
43
|
const start = performance.now();
|
|
20
44
|
setBaseAttributes(span, input);
|
|
21
45
|
try {
|
|
22
46
|
const output = await fn();
|
|
23
47
|
const duration = performance.now() - start;
|
|
48
|
+
const completion = processContent(output.completion);
|
|
24
49
|
span.setAttributes({
|
|
25
|
-
...(
|
|
26
|
-
[
|
|
50
|
+
...(completion !== undefined && {
|
|
51
|
+
[GEN_AI_ATTRS.COMPLETION]: completion,
|
|
27
52
|
}),
|
|
28
|
-
[
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
[
|
|
53
|
+
[GEN_AI_ATTRS.RESPONSE_MODEL]: input.model,
|
|
54
|
+
[GEN_AI_ATTRS.INPUT_TOKENS]: output.inputTokens,
|
|
55
|
+
[GEN_AI_ATTRS.OUTPUT_TOKENS]: output.outputTokens,
|
|
56
|
+
[GEN_AI_ATTRS.COST]: output.cost,
|
|
57
|
+
[GEN_AI_ATTRS.STATUS]: "success",
|
|
58
|
+
[GEN_AI_ATTRS.FINISH_REASONS]: ["stop"],
|
|
32
59
|
});
|
|
33
60
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
34
61
|
recordRequest(input.provider, input.model);
|
|
@@ -41,12 +68,11 @@ export async function traceLLMCall(input, fn) {
|
|
|
41
68
|
const duration = performance.now() - start;
|
|
42
69
|
const message = error instanceof Error ? error.message : String(error);
|
|
43
70
|
span.setAttributes({
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
48
|
-
[
|
|
49
|
-
[LLM_ATTRS.ERROR]: message,
|
|
71
|
+
[GEN_AI_ATTRS.INPUT_TOKENS]: 0,
|
|
72
|
+
[GEN_AI_ATTRS.OUTPUT_TOKENS]: 0,
|
|
73
|
+
[GEN_AI_ATTRS.COST]: 0,
|
|
74
|
+
[GEN_AI_ATTRS.STATUS]: "error",
|
|
75
|
+
[GEN_AI_ATTRS.ERROR]: message,
|
|
50
76
|
});
|
|
51
77
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
52
78
|
recordRequest(input.provider, input.model);
|
package/dist/tracer.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { ToadEyeConfig } from "./types.js";
|
|
1
|
+
import type { ToadEyeConfig } from "./types/index.js";
|
|
2
|
+
import "./instrumentations/openai.js";
|
|
3
|
+
import "./instrumentations/anthropic.js";
|
|
4
|
+
import "./instrumentations/gemini.js";
|
|
2
5
|
export declare function getConfig(): ToadEyeConfig | null;
|
|
3
6
|
export declare function initObservability(config: ToadEyeConfig): void;
|
|
4
7
|
export declare function shutdown(): Promise<void>;
|
package/dist/tracer.js
CHANGED
|
@@ -5,6 +5,11 @@ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
|
5
5
|
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
6
6
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
7
7
|
import { initMetrics } from "./metrics.js";
|
|
8
|
+
import { enableAll, disableAll } from "./instrumentations/registry.js";
|
|
9
|
+
// Side-effect imports: register provider instrumentations
|
|
10
|
+
import "./instrumentations/openai.js";
|
|
11
|
+
import "./instrumentations/anthropic.js";
|
|
12
|
+
import "./instrumentations/gemini.js";
|
|
8
13
|
const DEFAULT_ENDPOINT = "http://localhost:4318";
|
|
9
14
|
let sdk = null;
|
|
10
15
|
let currentConfig = null;
|
|
@@ -36,10 +41,14 @@ export function initObservability(config) {
|
|
|
36
41
|
sdk.start();
|
|
37
42
|
currentConfig = config;
|
|
38
43
|
initMetrics();
|
|
44
|
+
if (config.instrument?.length) {
|
|
45
|
+
enableAll(config.instrument);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
export async function shutdown() {
|
|
41
49
|
if (!sdk)
|
|
42
50
|
return;
|
|
51
|
+
disableAll();
|
|
43
52
|
await sdk.shutdown();
|
|
44
53
|
sdk = null;
|
|
45
54
|
currentConfig = null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span attribute keys following OTel GenAI semantic conventions.
|
|
3
|
+
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
4
|
+
*
|
|
5
|
+
* Standard attributes use `gen_ai.*` prefix.
|
|
6
|
+
* toad-eye extensions use `gen_ai.toad_eye.*` prefix.
|
|
7
|
+
*/
|
|
8
|
+
export declare const GEN_AI_ATTRS: {
|
|
9
|
+
readonly PROVIDER: "gen_ai.provider.name";
|
|
10
|
+
readonly REQUEST_MODEL: "gen_ai.request.model";
|
|
11
|
+
readonly RESPONSE_MODEL: "gen_ai.response.model";
|
|
12
|
+
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
13
|
+
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
14
|
+
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
15
|
+
readonly OPERATION: "gen_ai.operation.name";
|
|
16
|
+
readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
|
|
17
|
+
readonly ERROR: "error.type";
|
|
18
|
+
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
19
|
+
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
20
|
+
readonly COST: "gen_ai.toad_eye.cost";
|
|
21
|
+
readonly STATUS: "gen_ai.toad_eye.status";
|
|
22
|
+
readonly SESSION_ID: "session.id";
|
|
23
|
+
};
|
|
24
|
+
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
25
|
+
export declare const LLM_ATTRS: {
|
|
26
|
+
readonly PROVIDER: "gen_ai.provider.name";
|
|
27
|
+
readonly MODEL: "gen_ai.request.model";
|
|
28
|
+
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
29
|
+
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
30
|
+
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
31
|
+
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
32
|
+
readonly COST: "gen_ai.toad_eye.cost";
|
|
33
|
+
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
34
|
+
readonly STATUS: "gen_ai.toad_eye.status";
|
|
35
|
+
readonly ERROR: "error.type";
|
|
36
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span attribute keys following OTel GenAI semantic conventions.
|
|
3
|
+
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
4
|
+
*
|
|
5
|
+
* Standard attributes use `gen_ai.*` prefix.
|
|
6
|
+
* toad-eye extensions use `gen_ai.toad_eye.*` prefix.
|
|
7
|
+
*/
|
|
8
|
+
export const GEN_AI_ATTRS = {
|
|
9
|
+
// OTel GenAI standard
|
|
10
|
+
PROVIDER: "gen_ai.provider.name",
|
|
11
|
+
REQUEST_MODEL: "gen_ai.request.model",
|
|
12
|
+
RESPONSE_MODEL: "gen_ai.response.model",
|
|
13
|
+
INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
|
14
|
+
OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
|
|
15
|
+
TEMPERATURE: "gen_ai.request.temperature",
|
|
16
|
+
OPERATION: "gen_ai.operation.name",
|
|
17
|
+
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
|
18
|
+
ERROR: "error.type",
|
|
19
|
+
// toad-eye extensions
|
|
20
|
+
PROMPT: "gen_ai.toad_eye.prompt",
|
|
21
|
+
COMPLETION: "gen_ai.toad_eye.completion",
|
|
22
|
+
COST: "gen_ai.toad_eye.cost",
|
|
23
|
+
STATUS: "gen_ai.toad_eye.status",
|
|
24
|
+
SESSION_ID: "session.id",
|
|
25
|
+
};
|
|
26
|
+
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
27
|
+
export const LLM_ATTRS = {
|
|
28
|
+
PROVIDER: GEN_AI_ATTRS.PROVIDER,
|
|
29
|
+
MODEL: GEN_AI_ATTRS.REQUEST_MODEL,
|
|
30
|
+
PROMPT: GEN_AI_ATTRS.PROMPT,
|
|
31
|
+
COMPLETION: GEN_AI_ATTRS.COMPLETION,
|
|
32
|
+
INPUT_TOKENS: GEN_AI_ATTRS.INPUT_TOKENS,
|
|
33
|
+
OUTPUT_TOKENS: GEN_AI_ATTRS.OUTPUT_TOKENS,
|
|
34
|
+
COST: GEN_AI_ATTRS.COST,
|
|
35
|
+
TEMPERATURE: GEN_AI_ATTRS.TEMPERATURE,
|
|
36
|
+
STATUS: GEN_AI_ATTRS.STATUS,
|
|
37
|
+
ERROR: GEN_AI_ATTRS.ERROR,
|
|
38
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LLMProvider } from "./providers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for initObservability().
|
|
4
|
+
* This is what the user passes when connecting toad-eye to their service.
|
|
5
|
+
*/
|
|
6
|
+
export interface ToadEyeConfig {
|
|
7
|
+
readonly serviceName: string;
|
|
8
|
+
readonly endpoint?: string | undefined;
|
|
9
|
+
/** Set to false to disable recording prompt/completion text in spans. */
|
|
10
|
+
readonly recordContent?: boolean | undefined;
|
|
11
|
+
/** Record SHA-256 hash of content instead of plain text. Allows prompt comparison without reading. */
|
|
12
|
+
readonly hashContent?: boolean | undefined;
|
|
13
|
+
/** Regex patterns to redact from prompt/completion text before recording. */
|
|
14
|
+
readonly redactPatterns?: readonly RegExp[] | undefined;
|
|
15
|
+
readonly instrument?: readonly LLMProvider[] | undefined;
|
|
16
|
+
/** Static session ID — all spans will carry this value as `session.id`. */
|
|
17
|
+
readonly sessionId?: string | undefined;
|
|
18
|
+
/** Dynamic session ID extractor — called per traceLLMCall to resolve session ID. */
|
|
19
|
+
readonly sessionExtractor?: (() => string | undefined) | undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central type definitions for toad-eye.
|
|
3
|
+
*
|
|
4
|
+
* Organized by concern:
|
|
5
|
+
* - providers.ts — supported LLM provider names
|
|
6
|
+
* - config.ts — ToadEyeConfig (initObservability options)
|
|
7
|
+
* - attributes.ts — OTel span attribute keys (GEN_AI_ATTRS)
|
|
8
|
+
* - metrics.ts — OTel metric names (GEN_AI_METRICS)
|
|
9
|
+
* - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
|
|
10
|
+
*/
|
|
11
|
+
export declare const INSTRUMENTATION_NAME = "toad-eye";
|
|
12
|
+
export type { LLMProvider } from "./providers.js";
|
|
13
|
+
export type { ToadEyeConfig } from "./config.js";
|
|
14
|
+
export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
|
|
15
|
+
export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
|
|
16
|
+
export type { MetricName } from "./metrics.js";
|
|
17
|
+
export type { SpanStatus, LLMSpanAttributes } from "./spans.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central type definitions for toad-eye.
|
|
3
|
+
*
|
|
4
|
+
* Organized by concern:
|
|
5
|
+
* - providers.ts — supported LLM provider names
|
|
6
|
+
* - config.ts — ToadEyeConfig (initObservability options)
|
|
7
|
+
* - attributes.ts — OTel span attribute keys (GEN_AI_ATTRS)
|
|
8
|
+
* - metrics.ts — OTel metric names (GEN_AI_METRICS)
|
|
9
|
+
* - spans.ts — span-related types (LLMSpanAttributes, SpanStatus)
|
|
10
|
+
*/
|
|
11
|
+
export const INSTRUMENTATION_NAME = "toad-eye";
|
|
12
|
+
export { GEN_AI_ATTRS, LLM_ATTRS } from "./attributes.js";
|
|
13
|
+
export { GEN_AI_METRICS, LLM_METRICS } from "./metrics.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric names exported to Prometheus via OTel.
|
|
3
|
+
*
|
|
4
|
+
* Naming follows OTel GenAI semantic conventions:
|
|
5
|
+
* - `gen_ai.client.*` prefix for client-side metrics
|
|
6
|
+
* - Prometheus auto-converts dots to underscores
|
|
7
|
+
* - Prometheus adds _total suffix to counters automatically
|
|
8
|
+
*/
|
|
9
|
+
export declare const GEN_AI_METRICS: {
|
|
10
|
+
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
11
|
+
readonly TOKEN_USAGE: "gen_ai.client.token.usage";
|
|
12
|
+
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
13
|
+
readonly REQUESTS: "gen_ai.client.requests";
|
|
14
|
+
readonly ERRORS: "gen_ai.client.errors";
|
|
15
|
+
};
|
|
16
|
+
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
17
|
+
export declare const LLM_METRICS: {
|
|
18
|
+
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
19
|
+
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
20
|
+
readonly TOKENS: "gen_ai.client.token.usage";
|
|
21
|
+
readonly REQUESTS: "gen_ai.client.requests";
|
|
22
|
+
readonly ERRORS: "gen_ai.client.errors";
|
|
23
|
+
};
|
|
24
|
+
/** Type-safe metric name — only values from GEN_AI_METRICS are allowed. */
|
|
25
|
+
export type MetricName = (typeof GEN_AI_METRICS)[keyof typeof GEN_AI_METRICS];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric names exported to Prometheus via OTel.
|
|
3
|
+
*
|
|
4
|
+
* Naming follows OTel GenAI semantic conventions:
|
|
5
|
+
* - `gen_ai.client.*` prefix for client-side metrics
|
|
6
|
+
* - Prometheus auto-converts dots to underscores
|
|
7
|
+
* - Prometheus adds _total suffix to counters automatically
|
|
8
|
+
*/
|
|
9
|
+
export const GEN_AI_METRICS = {
|
|
10
|
+
REQUEST_DURATION: "gen_ai.client.operation.duration",
|
|
11
|
+
TOKEN_USAGE: "gen_ai.client.token.usage",
|
|
12
|
+
REQUEST_COST: "gen_ai.client.request.cost",
|
|
13
|
+
REQUESTS: "gen_ai.client.requests",
|
|
14
|
+
ERRORS: "gen_ai.client.errors",
|
|
15
|
+
};
|
|
16
|
+
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
17
|
+
export const LLM_METRICS = {
|
|
18
|
+
REQUEST_DURATION: GEN_AI_METRICS.REQUEST_DURATION,
|
|
19
|
+
REQUEST_COST: GEN_AI_METRICS.REQUEST_COST,
|
|
20
|
+
TOKENS: GEN_AI_METRICS.TOKEN_USAGE,
|
|
21
|
+
REQUESTS: GEN_AI_METRICS.REQUESTS,
|
|
22
|
+
ERRORS: GEN_AI_METRICS.ERRORS,
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LLMProvider } from "./providers.js";
|
|
2
|
+
/** Span status values used in trace attributes. */
|
|
3
|
+
export type SpanStatus = "success" | "error";
|
|
4
|
+
/**
|
|
5
|
+
* Attributes attached to every LLM span (trace).
|
|
6
|
+
* These become searchable fields in Jaeger and filterable dimensions in Grafana.
|
|
7
|
+
*/
|
|
8
|
+
export interface LLMSpanAttributes {
|
|
9
|
+
readonly provider: LLMProvider;
|
|
10
|
+
readonly model: string;
|
|
11
|
+
readonly prompt: string;
|
|
12
|
+
readonly completion: string;
|
|
13
|
+
readonly inputTokens: number;
|
|
14
|
+
readonly outputTokens: number;
|
|
15
|
+
readonly cost: number;
|
|
16
|
+
readonly temperature: number;
|
|
17
|
+
readonly status: SpanStatus;
|
|
18
|
+
readonly error?: string | undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const INSTRUMENTATION_NAME = "toad-eye";
|
|
2
2
|
/**
|
|
3
|
-
* LLM providers supported by toad-eye
|
|
3
|
+
* LLM providers supported by toad-eye.
|
|
4
|
+
* Values follow OTel GenAI semconv `gen_ai.provider.name`.
|
|
4
5
|
*/
|
|
5
6
|
export type LLMProvider = "anthropic" | "gemini" | "openai";
|
|
6
7
|
/**
|
|
@@ -26,44 +27,78 @@ export interface LLMSpanAttributes {
|
|
|
26
27
|
/**
|
|
27
28
|
* Metric names exported to Prometheus.
|
|
28
29
|
*
|
|
29
|
-
* Naming
|
|
30
|
-
* -
|
|
31
|
-
* - Prometheus auto-converts dots to underscores
|
|
32
|
-
* - Prometheus adds _total suffix to counters automatically
|
|
30
|
+
* Naming follows OTel GenAI semantic conventions where possible:
|
|
31
|
+
* - `gen_ai.client.*` prefix for client-side metrics
|
|
32
|
+
* - Prometheus auto-converts dots to underscores
|
|
33
|
+
* - Prometheus adds _total suffix to counters automatically
|
|
33
34
|
*/
|
|
35
|
+
export declare const GEN_AI_METRICS: {
|
|
36
|
+
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
37
|
+
readonly TOKEN_USAGE: "gen_ai.client.token.usage";
|
|
38
|
+
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
39
|
+
readonly REQUESTS: "gen_ai.client.requests";
|
|
40
|
+
readonly ERRORS: "gen_ai.client.errors";
|
|
41
|
+
};
|
|
42
|
+
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
34
43
|
export declare const LLM_METRICS: {
|
|
35
|
-
readonly REQUEST_DURATION: "
|
|
36
|
-
readonly REQUEST_COST: "
|
|
37
|
-
readonly TOKENS: "
|
|
38
|
-
readonly REQUESTS: "
|
|
39
|
-
readonly ERRORS: "
|
|
44
|
+
readonly REQUEST_DURATION: "gen_ai.client.operation.duration";
|
|
45
|
+
readonly REQUEST_COST: "gen_ai.client.request.cost";
|
|
46
|
+
readonly TOKENS: "gen_ai.client.token.usage";
|
|
47
|
+
readonly REQUESTS: "gen_ai.client.requests";
|
|
48
|
+
readonly ERRORS: "gen_ai.client.errors";
|
|
40
49
|
};
|
|
41
50
|
/**
|
|
42
|
-
* Span attribute keys
|
|
43
|
-
*
|
|
51
|
+
* Span attribute keys following OTel GenAI semantic conventions.
|
|
52
|
+
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
44
53
|
*/
|
|
54
|
+
export declare const GEN_AI_ATTRS: {
|
|
55
|
+
readonly PROVIDER: "gen_ai.provider.name";
|
|
56
|
+
readonly REQUEST_MODEL: "gen_ai.request.model";
|
|
57
|
+
readonly RESPONSE_MODEL: "gen_ai.response.model";
|
|
58
|
+
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
59
|
+
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
60
|
+
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
61
|
+
readonly OPERATION: "gen_ai.operation.name";
|
|
62
|
+
readonly FINISH_REASONS: "gen_ai.response.finish_reasons";
|
|
63
|
+
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
64
|
+
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
65
|
+
readonly COST: "gen_ai.toad_eye.cost";
|
|
66
|
+
readonly STATUS: "gen_ai.toad_eye.status";
|
|
67
|
+
readonly ERROR: "error.type";
|
|
68
|
+
readonly SESSION_ID: "session.id";
|
|
69
|
+
};
|
|
70
|
+
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
45
71
|
export declare const LLM_ATTRS: {
|
|
46
|
-
readonly PROVIDER: "
|
|
47
|
-
readonly MODEL: "
|
|
48
|
-
readonly PROMPT: "
|
|
49
|
-
readonly COMPLETION: "
|
|
50
|
-
readonly INPUT_TOKENS: "
|
|
51
|
-
readonly OUTPUT_TOKENS: "
|
|
52
|
-
readonly COST: "
|
|
53
|
-
readonly TEMPERATURE: "
|
|
54
|
-
readonly STATUS: "
|
|
55
|
-
readonly ERROR: "
|
|
72
|
+
readonly PROVIDER: "gen_ai.provider.name";
|
|
73
|
+
readonly MODEL: "gen_ai.request.model";
|
|
74
|
+
readonly PROMPT: "gen_ai.toad_eye.prompt";
|
|
75
|
+
readonly COMPLETION: "gen_ai.toad_eye.completion";
|
|
76
|
+
readonly INPUT_TOKENS: "gen_ai.usage.input_tokens";
|
|
77
|
+
readonly OUTPUT_TOKENS: "gen_ai.usage.output_tokens";
|
|
78
|
+
readonly COST: "gen_ai.toad_eye.cost";
|
|
79
|
+
readonly TEMPERATURE: "gen_ai.request.temperature";
|
|
80
|
+
readonly STATUS: "gen_ai.toad_eye.status";
|
|
81
|
+
readonly ERROR: "error.type";
|
|
56
82
|
};
|
|
57
83
|
/**
|
|
58
|
-
* Type-safe metric name
|
|
84
|
+
* Type-safe metric name
|
|
59
85
|
*/
|
|
60
|
-
export type MetricName = (typeof
|
|
86
|
+
export type MetricName = (typeof GEN_AI_METRICS)[keyof typeof GEN_AI_METRICS];
|
|
61
87
|
/**
|
|
62
88
|
* Configuration for initObservability().
|
|
63
|
-
* This is what the user passes when connecting toad-eye to their service.
|
|
64
89
|
*/
|
|
65
90
|
export interface ToadEyeConfig {
|
|
66
91
|
readonly serviceName: string;
|
|
67
92
|
readonly endpoint?: string | undefined;
|
|
93
|
+
/** Set to false to disable recording prompt/completion text in spans. */
|
|
68
94
|
readonly recordContent?: boolean | undefined;
|
|
95
|
+
/** Record SHA-256 hash of content instead of plain text. Allows prompt comparison without reading. */
|
|
96
|
+
readonly hashContent?: boolean | undefined;
|
|
97
|
+
/** Regex patterns to redact from prompt/completion text before recording. */
|
|
98
|
+
readonly redactPatterns?: readonly RegExp[] | undefined;
|
|
99
|
+
readonly instrument?: readonly LLMProvider[] | undefined;
|
|
100
|
+
/** Static session ID — all spans will carry this value as `session.id`. */
|
|
101
|
+
readonly sessionId?: string | undefined;
|
|
102
|
+
/** Dynamic session ID extractor — called per traceLLMCall to resolve session ID. */
|
|
103
|
+
readonly sessionExtractor?: (() => string | undefined) | undefined;
|
|
69
104
|
}
|
package/dist/types.js
CHANGED
|
@@ -2,31 +2,57 @@ export const INSTRUMENTATION_NAME = "toad-eye";
|
|
|
2
2
|
/**
|
|
3
3
|
* Metric names exported to Prometheus.
|
|
4
4
|
*
|
|
5
|
-
* Naming
|
|
6
|
-
* -
|
|
7
|
-
* - Prometheus auto-converts dots to underscores
|
|
8
|
-
* - Prometheus adds _total suffix to counters automatically
|
|
5
|
+
* Naming follows OTel GenAI semantic conventions where possible:
|
|
6
|
+
* - `gen_ai.client.*` prefix for client-side metrics
|
|
7
|
+
* - Prometheus auto-converts dots to underscores
|
|
8
|
+
* - Prometheus adds _total suffix to counters automatically
|
|
9
9
|
*/
|
|
10
|
+
export const GEN_AI_METRICS = {
|
|
11
|
+
REQUEST_DURATION: "gen_ai.client.operation.duration",
|
|
12
|
+
TOKEN_USAGE: "gen_ai.client.token.usage",
|
|
13
|
+
REQUEST_COST: "gen_ai.client.request.cost",
|
|
14
|
+
REQUESTS: "gen_ai.client.requests",
|
|
15
|
+
ERRORS: "gen_ai.client.errors",
|
|
16
|
+
};
|
|
17
|
+
/** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
|
|
10
18
|
export const LLM_METRICS = {
|
|
11
|
-
REQUEST_DURATION:
|
|
12
|
-
REQUEST_COST:
|
|
13
|
-
TOKENS:
|
|
14
|
-
REQUESTS:
|
|
15
|
-
ERRORS:
|
|
19
|
+
REQUEST_DURATION: GEN_AI_METRICS.REQUEST_DURATION,
|
|
20
|
+
REQUEST_COST: GEN_AI_METRICS.REQUEST_COST,
|
|
21
|
+
TOKENS: GEN_AI_METRICS.TOKEN_USAGE,
|
|
22
|
+
REQUESTS: GEN_AI_METRICS.REQUESTS,
|
|
23
|
+
ERRORS: GEN_AI_METRICS.ERRORS,
|
|
16
24
|
};
|
|
17
25
|
/**
|
|
18
|
-
* Span attribute keys
|
|
19
|
-
*
|
|
26
|
+
* Span attribute keys following OTel GenAI semantic conventions.
|
|
27
|
+
* See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
20
28
|
*/
|
|
29
|
+
export const GEN_AI_ATTRS = {
|
|
30
|
+
PROVIDER: "gen_ai.provider.name",
|
|
31
|
+
REQUEST_MODEL: "gen_ai.request.model",
|
|
32
|
+
RESPONSE_MODEL: "gen_ai.response.model",
|
|
33
|
+
INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
|
34
|
+
OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
|
|
35
|
+
TEMPERATURE: "gen_ai.request.temperature",
|
|
36
|
+
OPERATION: "gen_ai.operation.name",
|
|
37
|
+
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
|
38
|
+
// Not in OTel semconv yet — toad-eye extensions
|
|
39
|
+
PROMPT: "gen_ai.toad_eye.prompt",
|
|
40
|
+
COMPLETION: "gen_ai.toad_eye.completion",
|
|
41
|
+
COST: "gen_ai.toad_eye.cost",
|
|
42
|
+
STATUS: "gen_ai.toad_eye.status",
|
|
43
|
+
ERROR: "error.type",
|
|
44
|
+
SESSION_ID: "session.id",
|
|
45
|
+
};
|
|
46
|
+
/** @deprecated Use GEN_AI_ATTRS instead. Kept for backward compatibility. */
|
|
21
47
|
export const LLM_ATTRS = {
|
|
22
|
-
PROVIDER:
|
|
23
|
-
MODEL:
|
|
24
|
-
PROMPT:
|
|
25
|
-
COMPLETION:
|
|
26
|
-
INPUT_TOKENS:
|
|
27
|
-
OUTPUT_TOKENS:
|
|
28
|
-
COST:
|
|
29
|
-
TEMPERATURE:
|
|
30
|
-
STATUS:
|
|
31
|
-
ERROR:
|
|
48
|
+
PROVIDER: GEN_AI_ATTRS.PROVIDER,
|
|
49
|
+
MODEL: GEN_AI_ATTRS.REQUEST_MODEL,
|
|
50
|
+
PROMPT: GEN_AI_ATTRS.PROMPT,
|
|
51
|
+
COMPLETION: GEN_AI_ATTRS.COMPLETION,
|
|
52
|
+
INPUT_TOKENS: GEN_AI_ATTRS.INPUT_TOKENS,
|
|
53
|
+
OUTPUT_TOKENS: GEN_AI_ATTRS.OUTPUT_TOKENS,
|
|
54
|
+
COST: GEN_AI_ATTRS.COST,
|
|
55
|
+
TEMPERATURE: GEN_AI_ATTRS.TEMPERATURE,
|
|
56
|
+
STATUS: GEN_AI_ATTRS.STATUS,
|
|
57
|
+
ERROR: GEN_AI_ATTRS.ERROR,
|
|
32
58
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toad-eye",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Auto-instrumentation, cost tracking, and observability for LLM services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,13 @@
|
|
|
30
30
|
"observability",
|
|
31
31
|
"tracing",
|
|
32
32
|
"metrics",
|
|
33
|
-
"grafana"
|
|
33
|
+
"grafana",
|
|
34
|
+
"openai",
|
|
35
|
+
"anthropic",
|
|
36
|
+
"gemini",
|
|
37
|
+
"cost-tracking",
|
|
38
|
+
"auto-instrumentation",
|
|
39
|
+
"gen-ai"
|
|
34
40
|
],
|
|
35
41
|
"license": "ISC",
|
|
36
42
|
"dependencies": {
|
|
@@ -40,6 +46,11 @@
|
|
|
40
46
|
"@opentelemetry/resources": "^2.6.0",
|
|
41
47
|
"@opentelemetry/sdk-metrics": "^2.6.0",
|
|
42
48
|
"@opentelemetry/sdk-node": "^0.213.0",
|
|
43
|
-
"@opentelemetry/semantic-conventions": "^1.40.0"
|
|
49
|
+
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
50
|
+
"nodemailer": "^6.9.0",
|
|
51
|
+
"yaml": "^2.7.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/nodemailer": "^7.0.11"
|
|
44
55
|
}
|
|
45
56
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# toad-eye alerts configuration
|
|
2
|
+
# Copy to alerts.yml and customize
|
|
3
|
+
|
|
4
|
+
# prometheusUrl: http://localhost:9090
|
|
5
|
+
# grafanaUrl: http://localhost:3100
|
|
6
|
+
# evalIntervalSeconds: 30
|
|
7
|
+
# cooldownMinutes: 30
|
|
8
|
+
|
|
9
|
+
# channels:
|
|
10
|
+
# telegram:
|
|
11
|
+
# type: telegram
|
|
12
|
+
# token: "BOT_TOKEN"
|
|
13
|
+
# chatId: "CHAT_ID"
|
|
14
|
+
# slack:
|
|
15
|
+
# type: slack_webhook
|
|
16
|
+
# url: "https://hooks.slack.com/services/..."
|
|
17
|
+
|
|
18
|
+
alerts:
|
|
19
|
+
# Cost spike: $10 in 1 hour
|
|
20
|
+
- name: cost_spike
|
|
21
|
+
metric: gen_ai.client.request.cost
|
|
22
|
+
condition: sum_1h > 10
|
|
23
|
+
channels: [slack]
|
|
24
|
+
|
|
25
|
+
# Daily budget: $50 in 24 hours
|
|
26
|
+
- name: budget_daily
|
|
27
|
+
metric: gen_ai.client.request.cost
|
|
28
|
+
condition: sum_24h > 50
|
|
29
|
+
channels: [slack]
|
|
30
|
+
|
|
31
|
+
# Latency anomaly: p95 increased >50% vs 7-day baseline
|
|
32
|
+
- name: latency_anomaly
|
|
33
|
+
metric: gen_ai.client.operation.duration
|
|
34
|
+
condition: p95_pct_5m_7d > 50
|
|
35
|
+
channels: [slack]
|
|
36
|
+
|
|
37
|
+
# Latency critical: p95 increased >100% vs 7-day baseline
|
|
38
|
+
- name: latency_critical
|
|
39
|
+
metric: gen_ai.client.operation.duration
|
|
40
|
+
condition: p95_pct_5m_7d > 100
|
|
41
|
+
channels: [slack]
|
|
42
|
+
|
|
43
|
+
# Error rate warning: >5% over 15 minutes
|
|
44
|
+
- name: error_rate_warning
|
|
45
|
+
metric: gen_ai.client.errors
|
|
46
|
+
condition: ratio_15m > 0.05
|
|
47
|
+
channels: [slack]
|
|
48
|
+
|
|
49
|
+
# Error rate critical: >20% over 5 minutes (provider likely down)
|
|
50
|
+
- name: error_rate_critical
|
|
51
|
+
metric: gen_ai.client.errors
|
|
52
|
+
condition: ratio_5m > 0.2
|
|
53
|
+
channels: [slack]
|
|
54
|
+
cooldown: 15
|
|
@@ -28,8 +28,7 @@ services:
|
|
|
28
28
|
image: grafana/grafana:latest
|
|
29
29
|
volumes:
|
|
30
30
|
- ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
|
|
31
|
-
- ./grafana/dashboards
|
|
32
|
-
- ./grafana/dashboards/llm-overview.json:/etc/grafana/provisioning/dashboards/llm-overview.json
|
|
31
|
+
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
|
|
33
32
|
environment:
|
|
34
33
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
|
35
34
|
- GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/llm-overview.json
|