@juspay/neurolink 12.34.3 → 12.35.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/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +341 -341
- package/dist/proxy/proxyIR.d.ts +44 -0
- package/dist/proxy/proxyIR.js +127 -0
- package/dist/telemetry/telemetryService.d.ts +21 -0
- package/dist/telemetry/telemetryService.js +200 -14
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxyIR.d.ts +283 -0
- package/dist/types/proxyIR.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exhaustiveness enforcement for the translation IR.
|
|
3
|
+
*
|
|
4
|
+
* A translation layer fails quietly: an unhandled content part or event variant
|
|
5
|
+
* becomes a missing tool call, and the only symptom is the next hop rejecting a
|
|
6
|
+
* request nobody can explain. `assertProxyIRExhaustive` makes that a compile
|
|
7
|
+
* error instead. Passing a value whose type is not `never` does not type-check,
|
|
8
|
+
* so a new IR variant breaks every `switch` that has not handled it.
|
|
9
|
+
*
|
|
10
|
+
* It throws at runtime as well, because the compile-time guarantee only holds for
|
|
11
|
+
* values that really are typed. A wire payload narrowed from `unknown` can reach a
|
|
12
|
+
* default branch at runtime, and failing loudly there beats translating a request
|
|
13
|
+
* with a silently dropped element.
|
|
14
|
+
*/
|
|
15
|
+
import type { ProxyIRContentPart, ProxyIRResponseEvent, ProxyIRTerminalOutcome, ProxyIRUnmappableValue } from "../types/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* Refuse an unhandled IR variant.
|
|
18
|
+
*
|
|
19
|
+
* `context` names the switch that failed, since the value alone rarely says which
|
|
20
|
+
* of several exhaustive switches over the same union let it through.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertProxyIRExhaustive(value: never, context: string): never;
|
|
23
|
+
/** Mark a wire value no codec could represent, preserving it for the caller. */
|
|
24
|
+
export declare function proxyIRUnmappable(sourceKind: string, reason: string, raw: unknown): ProxyIRUnmappableValue;
|
|
25
|
+
/** Narrow a collected codec result to the unmappable marker. */
|
|
26
|
+
export declare function isProxyIRUnmappable(value: unknown): value is ProxyIRUnmappableValue;
|
|
27
|
+
/**
|
|
28
|
+
* Name a content part for a diagnostic.
|
|
29
|
+
*
|
|
30
|
+
* This lives in `src` rather than in the suite that asserts on it because
|
|
31
|
+
* `tsconfig.json` excludes `test`: an exhaustive switch written only in a test
|
|
32
|
+
* gives no compile-time guarantee at all. These describers are the shipped
|
|
33
|
+
* consumers that pin every union, so adding an IR variant fails `tsc --noEmit`
|
|
34
|
+
* here before it can reach a codec that silently drops it.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately excludes the payload of text, thinking and tool arguments: a
|
|
37
|
+
* describer's output reaches logs and span attributes, and prompt content does
|
|
38
|
+
* not belong there.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeProxyIRPart(part: ProxyIRContentPart): string;
|
|
41
|
+
/** Name a stream event for a diagnostic, without its payload. */
|
|
42
|
+
export declare function describeProxyIRResponseEvent(event: ProxyIRResponseEvent): string;
|
|
43
|
+
/** Name a terminal outcome, keeping an unmodelled upstream stop reason visible. */
|
|
44
|
+
export declare function describeProxyIRTerminalOutcome(outcome: ProxyIRTerminalOutcome): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exhaustiveness enforcement for the translation IR.
|
|
3
|
+
*
|
|
4
|
+
* A translation layer fails quietly: an unhandled content part or event variant
|
|
5
|
+
* becomes a missing tool call, and the only symptom is the next hop rejecting a
|
|
6
|
+
* request nobody can explain. `assertProxyIRExhaustive` makes that a compile
|
|
7
|
+
* error instead. Passing a value whose type is not `never` does not type-check,
|
|
8
|
+
* so a new IR variant breaks every `switch` that has not handled it.
|
|
9
|
+
*
|
|
10
|
+
* It throws at runtime as well, because the compile-time guarantee only holds for
|
|
11
|
+
* values that really are typed. A wire payload narrowed from `unknown` can reach a
|
|
12
|
+
* default branch at runtime, and failing loudly there beats translating a request
|
|
13
|
+
* with a silently dropped element.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Refuse an unhandled IR variant.
|
|
17
|
+
*
|
|
18
|
+
* `context` names the switch that failed, since the value alone rarely says which
|
|
19
|
+
* of several exhaustive switches over the same union let it through.
|
|
20
|
+
*/
|
|
21
|
+
export function assertProxyIRExhaustive(value, context) {
|
|
22
|
+
throw new Error(`[proxy-ir] ${context} did not handle variant: ${describeProxyIRVariant(value)}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Describe a value for the exhaustiveness error without assuming its shape.
|
|
26
|
+
*
|
|
27
|
+
* The parameter is `never` at the call site, so nothing about it can be trusted
|
|
28
|
+
* here: it may be a primitive, or an object with no discriminant at all — and an
|
|
29
|
+
* untagged object is exactly the shape a malformed wire payload takes. Reviewed
|
|
30
|
+
* finding (PR #1826): serializing that object's fields defeated the point of a
|
|
31
|
+
* log/span-safe diagnostic, since the object can carry the same text or tool
|
|
32
|
+
* arguments the typed describers deliberately withhold. The fallback is now a
|
|
33
|
+
* fixed, shape-only marker — never the object's own content — for every
|
|
34
|
+
* untagged case, not only the ones this file happens to call today.
|
|
35
|
+
*/
|
|
36
|
+
function describeProxyIRVariant(value) {
|
|
37
|
+
if (value === null || typeof value !== "object") {
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
const record = value;
|
|
41
|
+
for (const discriminant of ["kind", "status", "source", "format"]) {
|
|
42
|
+
const tag = record[discriminant];
|
|
43
|
+
if (typeof tag === "string") {
|
|
44
|
+
return `${discriminant}=${tag}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return "[object without recognized discriminant]";
|
|
48
|
+
}
|
|
49
|
+
/** Mark a wire value no codec could represent, preserving it for the caller. */
|
|
50
|
+
export function proxyIRUnmappable(sourceKind, reason, raw) {
|
|
51
|
+
return { proxyIRUnmappable: true, sourceKind, reason, raw };
|
|
52
|
+
}
|
|
53
|
+
/** Narrow a collected codec result to the unmappable marker. */
|
|
54
|
+
export function isProxyIRUnmappable(value) {
|
|
55
|
+
return (value !== null &&
|
|
56
|
+
typeof value === "object" &&
|
|
57
|
+
value.proxyIRUnmappable === true);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Name a content part for a diagnostic.
|
|
61
|
+
*
|
|
62
|
+
* This lives in `src` rather than in the suite that asserts on it because
|
|
63
|
+
* `tsconfig.json` excludes `test`: an exhaustive switch written only in a test
|
|
64
|
+
* gives no compile-time guarantee at all. These describers are the shipped
|
|
65
|
+
* consumers that pin every union, so adding an IR variant fails `tsc --noEmit`
|
|
66
|
+
* here before it can reach a codec that silently drops it.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately excludes the payload of text, thinking and tool arguments: a
|
|
69
|
+
* describer's output reaches logs and span attributes, and prompt content does
|
|
70
|
+
* not belong there.
|
|
71
|
+
*/
|
|
72
|
+
export function describeProxyIRPart(part) {
|
|
73
|
+
switch (part.kind) {
|
|
74
|
+
case "text":
|
|
75
|
+
return `text(${part.text.length}b)`;
|
|
76
|
+
case "thinking":
|
|
77
|
+
return `thinking(${part.text.length}b)`;
|
|
78
|
+
case "image":
|
|
79
|
+
return `image(${part.encoding})`;
|
|
80
|
+
case "tool_call":
|
|
81
|
+
return `tool_call(${part.toolName})`;
|
|
82
|
+
case "tool_result":
|
|
83
|
+
return `tool_result(${part.content.length} parts${part.isError ? ", error" : ""})`;
|
|
84
|
+
case "unmapped":
|
|
85
|
+
return `unmapped(${part.sourceKind}: ${part.reason})`;
|
|
86
|
+
default:
|
|
87
|
+
return assertProxyIRExhaustive(part, "describeProxyIRPart");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Name a stream event for a diagnostic, without its payload. */
|
|
91
|
+
export function describeProxyIRResponseEvent(event) {
|
|
92
|
+
switch (event.kind) {
|
|
93
|
+
case "text_delta":
|
|
94
|
+
return `text_delta(${event.text.length}b)`;
|
|
95
|
+
case "thinking_delta":
|
|
96
|
+
return `thinking_delta(${event.text.length}b)`;
|
|
97
|
+
case "tool_call_start":
|
|
98
|
+
return `tool_call_start(${event.toolName})`;
|
|
99
|
+
case "tool_call_arguments_delta":
|
|
100
|
+
return `tool_call_arguments_delta(${event.partialArguments.length}b)`;
|
|
101
|
+
case "tool_call_done":
|
|
102
|
+
return "tool_call_done";
|
|
103
|
+
case "item_done":
|
|
104
|
+
return "item_done";
|
|
105
|
+
case "usage":
|
|
106
|
+
return "usage";
|
|
107
|
+
case "terminal":
|
|
108
|
+
return `terminal(${describeProxyIRTerminalOutcome(event.outcome)})`;
|
|
109
|
+
default:
|
|
110
|
+
return assertProxyIRExhaustive(event, "describeProxyIRResponseEvent");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Name a terminal outcome, keeping an unmodelled upstream stop reason visible. */
|
|
114
|
+
export function describeProxyIRTerminalOutcome(outcome) {
|
|
115
|
+
switch (outcome.status) {
|
|
116
|
+
case "completed":
|
|
117
|
+
return outcome.finishReason === "other"
|
|
118
|
+
? `completed:${outcome.rawFinishReason}`
|
|
119
|
+
: `completed:${outcome.finishReason}`;
|
|
120
|
+
case "failed":
|
|
121
|
+
return `failed:${outcome.error.code}`;
|
|
122
|
+
case "incomplete":
|
|
123
|
+
return `incomplete:${outcome.reason}`;
|
|
124
|
+
default:
|
|
125
|
+
return assertProxyIRExhaustive(outcome, "describeProxyIRTerminalOutcome");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -20,6 +20,14 @@ export declare class TelemetryService {
|
|
|
20
20
|
private requestCount;
|
|
21
21
|
private totalResponseTime;
|
|
22
22
|
private responseTimeCount;
|
|
23
|
+
private static readonly MAX_PENDING_CALLS;
|
|
24
|
+
private pendingCalls;
|
|
25
|
+
private isReady;
|
|
26
|
+
private droppedPendingCallWarned;
|
|
27
|
+
/** The in-flight (or already-settled) async init. Never rejects — see
|
|
28
|
+
* initializeTelemetry()'s catch block, which always resolves normally.
|
|
29
|
+
* `initialize()`/`shutdown()` await it so they don't race construction. */
|
|
30
|
+
private readyPromise?;
|
|
23
31
|
private constructor();
|
|
24
32
|
static getInstance(): TelemetryService;
|
|
25
33
|
private isTelemetryEnabled;
|
|
@@ -27,6 +35,11 @@ export declare class TelemetryService {
|
|
|
27
35
|
private adoptExternalTracerProvider;
|
|
28
36
|
private initializeTelemetry;
|
|
29
37
|
private initializeMetrics;
|
|
38
|
+
private bufferPendingCall;
|
|
39
|
+
/** Instruments now exist — drain anything buffered while we waited, in
|
|
40
|
+
* order, then flip fully sync for every call after this one. */
|
|
41
|
+
private markReady;
|
|
42
|
+
private discardPendingCalls;
|
|
30
43
|
initialize(): Promise<void>;
|
|
31
44
|
/**
|
|
32
45
|
* @deprecated Vercel AI SDK's experimental_telemetry creates ai.generateText/ai.streamText
|
|
@@ -36,13 +49,21 @@ export declare class TelemetryService {
|
|
|
36
49
|
*/
|
|
37
50
|
traceAIRequest<T>(provider: string, operation: () => Promise<T>, operationType?: string): Promise<T>;
|
|
38
51
|
recordAIRequest(provider: string, model: string, tokens: number, duration: number, cost?: number): void;
|
|
52
|
+
private emitAIRequest;
|
|
39
53
|
recordAIError(provider: string, error: Error): void;
|
|
54
|
+
private emitAIError;
|
|
40
55
|
recordMCPToolCall(toolName: string, duration: number, success: boolean): void;
|
|
56
|
+
private emitMCPToolCall;
|
|
41
57
|
recordConnection(type: "websocket" | "sse" | "http"): void;
|
|
58
|
+
private emitConnection;
|
|
42
59
|
recordConnectionClosed(type: "websocket" | "sse" | "http"): void;
|
|
60
|
+
private emitConnectionClosed;
|
|
43
61
|
recordResponseTime(endpoint: string, method: string, duration: number): void;
|
|
62
|
+
private emitResponseTime;
|
|
44
63
|
recordCustomMetric(name: string, value: number, labels?: Record<string, string>): void;
|
|
64
|
+
private emitCustomMetric;
|
|
45
65
|
recordCustomHistogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
66
|
+
private emitCustomHistogram;
|
|
46
67
|
getHealthMetrics(): Promise<HealthMetrics>;
|
|
47
68
|
isEnabled(): boolean;
|
|
48
69
|
getStatus(): {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { context, metrics, trace, } from "@opentelemetry/api";
|
|
2
|
-
import { BasicTracerProvider, BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
3
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
5
|
-
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
6
2
|
import { logger } from "../utils/logger.js";
|
|
7
3
|
export class TelemetryService {
|
|
8
4
|
static instance;
|
|
@@ -27,17 +23,52 @@ export class TelemetryService {
|
|
|
27
23
|
requestCount = 0;
|
|
28
24
|
totalResponseTime = 0;
|
|
29
25
|
responseTimeCount = 0;
|
|
26
|
+
// Async-init readiness + buffering. Instruments above are constructed by
|
|
27
|
+
// an async initializeTelemetry() that the constructor fires but does not
|
|
28
|
+
// await (see getInstance()'s comment). A recording call can therefore
|
|
29
|
+
// legitimately arrive while telemetry is enabled but the instruments
|
|
30
|
+
// don't exist yet — that is NOT the same as telemetry being disabled, so
|
|
31
|
+
// it must not be dropped silently. `pendingCalls` buffers the OTEL side
|
|
32
|
+
// effect of such a call (never the whole method — see each recordX
|
|
33
|
+
// method's emitX split) until `markReady()` drains it. The cap and
|
|
34
|
+
// rate-limited warning below exist only for a pathological case (init
|
|
35
|
+
// that never resolves); the common case drains within milliseconds.
|
|
36
|
+
static MAX_PENDING_CALLS = 200;
|
|
37
|
+
pendingCalls = [];
|
|
38
|
+
isReady = false;
|
|
39
|
+
droppedPendingCallWarned = false;
|
|
40
|
+
/** The in-flight (or already-settled) async init. Never rejects — see
|
|
41
|
+
* initializeTelemetry()'s catch block, which always resolves normally.
|
|
42
|
+
* `initialize()`/`shutdown()` await it so they don't race construction. */
|
|
43
|
+
readyPromise;
|
|
30
44
|
constructor() {
|
|
31
45
|
// Check if telemetry is enabled
|
|
32
46
|
this.enabled = this.isTelemetryEnabled();
|
|
33
47
|
if (this.enabled) {
|
|
34
|
-
|
|
48
|
+
// initializeTelemetry() is async — it dynamically imports the OTel
|
|
49
|
+
// SDK packages and constructs the tracer/meter/instruments — but it
|
|
50
|
+
// is fired here WITHOUT an await. The constructor itself must stay
|
|
51
|
+
// fully synchronous and non-yielding, because getInstance() below is
|
|
52
|
+
// a plain, synchronous check-then-assign with no await in between.
|
|
53
|
+
// That is only race-free as long as nothing here yields control back
|
|
54
|
+
// to the event loop before `TelemetryService.instance` is assigned.
|
|
55
|
+
// Awaiting anything in the constructor (or making getInstance()
|
|
56
|
+
// async) would let two concurrent getInstance() callers both observe
|
|
57
|
+
// "not yet assigned" and each construct their own instance — the
|
|
58
|
+
// exact double-construction race that
|
|
59
|
+
// observability/instrumentation.ts's initializeOpenTelemetry() already
|
|
60
|
+
// hit and had to fix with a shared in-flight promise (see the
|
|
61
|
+
// "Initialize OpenTelemetry once and let concurrent callers share the
|
|
62
|
+
// work" comment there). Do not reintroduce it here.
|
|
63
|
+
this.readyPromise = this.initializeTelemetry();
|
|
35
64
|
}
|
|
36
65
|
else {
|
|
37
66
|
logger.debug("[Telemetry] Disabled - set NEUROLINK_TELEMETRY_ENABLED=true or configure OTEL_EXPORTER_OTLP_ENDPOINT to enable");
|
|
38
67
|
}
|
|
39
68
|
}
|
|
40
69
|
static getInstance() {
|
|
70
|
+
// Synchronous check-then-assign — see the constructor's comment above
|
|
71
|
+
// for why this must never gain an await before the assignment.
|
|
41
72
|
if (!TelemetryService.instance) {
|
|
42
73
|
TelemetryService.instance = new TelemetryService();
|
|
43
74
|
}
|
|
@@ -84,12 +115,19 @@ export class TelemetryService {
|
|
|
84
115
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
85
116
|
});
|
|
86
117
|
}
|
|
87
|
-
initializeTelemetry() {
|
|
118
|
+
async initializeTelemetry() {
|
|
88
119
|
try {
|
|
89
120
|
if (this.hasExternalTracerProvider()) {
|
|
90
121
|
this.adoptExternalTracerProvider("global tracer provider already registered");
|
|
122
|
+
this.markReady();
|
|
91
123
|
return;
|
|
92
124
|
}
|
|
125
|
+
const [{ BasicTracerProvider, BatchSpanProcessor }, { OTLPTraceExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION },] = await Promise.all([
|
|
126
|
+
import("@opentelemetry/sdk-trace-base"),
|
|
127
|
+
import("@opentelemetry/exporter-trace-otlp-http"),
|
|
128
|
+
import("@opentelemetry/resources"),
|
|
129
|
+
import("@opentelemetry/semantic-conventions"),
|
|
130
|
+
]);
|
|
93
131
|
const resource = resourceFromAttributes({
|
|
94
132
|
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "neurolink-ai",
|
|
95
133
|
[ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || "3.0.1",
|
|
@@ -106,6 +144,7 @@ export class TelemetryService {
|
|
|
106
144
|
this.meter = metrics.getMeter("neurolink-ai");
|
|
107
145
|
this.tracer = this.tracerProvider.getTracer("neurolink-ai");
|
|
108
146
|
this.initializeMetrics();
|
|
147
|
+
this.markReady();
|
|
109
148
|
logger.debug("[Telemetry] Initialized local telemetry exporter", {
|
|
110
149
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
111
150
|
globalTracerProviderOwnedBy: "observability/instrumentation",
|
|
@@ -118,10 +157,16 @@ export class TelemetryService {
|
|
|
118
157
|
errorMessage.includes("already set");
|
|
119
158
|
if (isDuplicateRegistration && this.hasExternalTracerProvider()) {
|
|
120
159
|
this.adoptExternalTracerProvider("duplicate global tracer registration detected");
|
|
160
|
+
this.markReady();
|
|
121
161
|
return;
|
|
122
162
|
}
|
|
123
163
|
logger.error("[Telemetry] Failed to initialize:", error);
|
|
124
164
|
this.enabled = false;
|
|
165
|
+
// No instruments exist and none ever will for this instance — there
|
|
166
|
+
// is nothing left to drain a buffered call against, so the buffer is
|
|
167
|
+
// cleared rather than drained. Every recording method's `!this.enabled`
|
|
168
|
+
// guard now makes it a no-op anyway.
|
|
169
|
+
this.discardPendingCalls();
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
initializeMetrics() {
|
|
@@ -153,10 +198,57 @@ export class TelemetryService {
|
|
|
153
198
|
description: "Response time in milliseconds",
|
|
154
199
|
});
|
|
155
200
|
}
|
|
201
|
+
// ============================================================
|
|
202
|
+
// Pending-call buffer (see the field comment above pendingCalls)
|
|
203
|
+
// ============================================================
|
|
204
|
+
bufferPendingCall(call) {
|
|
205
|
+
if (this.pendingCalls.length >= TelemetryService.MAX_PENDING_CALLS) {
|
|
206
|
+
// Drop the oldest, not the newest — recent calls are more likely to
|
|
207
|
+
// still matter to whoever is about to read a dashboard.
|
|
208
|
+
this.pendingCalls.shift();
|
|
209
|
+
if (!this.droppedPendingCallWarned) {
|
|
210
|
+
this.droppedPendingCallWarned = true;
|
|
211
|
+
logger.warn(`[Telemetry] Pending metric buffer exceeded ${TelemetryService.MAX_PENDING_CALLS} entries — dropping the oldest buffered call(s). Logged once, not per drop: telemetry initialization may be unusually slow or stalled.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.pendingCalls.push(call);
|
|
215
|
+
}
|
|
216
|
+
/** Instruments now exist — drain anything buffered while we waited, in
|
|
217
|
+
* order, then flip fully sync for every call after this one. */
|
|
218
|
+
markReady() {
|
|
219
|
+
this.isReady = true;
|
|
220
|
+
if (this.pendingCalls.length === 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const buffered = this.pendingCalls;
|
|
224
|
+
this.pendingCalls = [];
|
|
225
|
+
for (const call of buffered) {
|
|
226
|
+
try {
|
|
227
|
+
call();
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.warn("[Telemetry] A buffered metric call failed during drain:", error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
discardPendingCalls() {
|
|
235
|
+
this.pendingCalls = [];
|
|
236
|
+
}
|
|
156
237
|
async initialize() {
|
|
157
238
|
if (!this.enabled) {
|
|
158
239
|
return;
|
|
159
240
|
}
|
|
241
|
+
// The tracer/meter/instruments are constructed asynchronously (see the
|
|
242
|
+
// constructor's comment). Wait for that to settle before inspecting
|
|
243
|
+
// usingExternalTracerProvider/tracerProvider below, so this method's
|
|
244
|
+
// behavior doesn't depend on how many event-loop ticks have passed
|
|
245
|
+
// since getInstance() was called — without this, a caller invoking
|
|
246
|
+
// initialize() right after getInstance() would almost always observe
|
|
247
|
+
// `!this.tracerProvider` and return early, silently skipping the
|
|
248
|
+
// AsyncLocalStorageContextManager registration below.
|
|
249
|
+
if (this.readyPromise) {
|
|
250
|
+
await this.readyPromise;
|
|
251
|
+
}
|
|
160
252
|
if (this.usingExternalTracerProvider) {
|
|
161
253
|
this.initialized = true;
|
|
162
254
|
logger.debug("[Telemetry] External TracerProvider already initialized by host");
|
|
@@ -227,7 +319,21 @@ export class TelemetryService {
|
|
|
227
319
|
this.requestCount++;
|
|
228
320
|
this.totalResponseTime += duration;
|
|
229
321
|
this.responseTimeCount++;
|
|
230
|
-
if (!this.enabled
|
|
322
|
+
if (!this.enabled) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!this.isReady) {
|
|
326
|
+
// Instruments aren't constructed yet — buffer the OTEL side effect
|
|
327
|
+
// only (emitAIRequest), never this whole method: the runtime
|
|
328
|
+
// counters above must fire exactly once per call, not again when the
|
|
329
|
+
// buffer drains.
|
|
330
|
+
this.bufferPendingCall(() => this.emitAIRequest(provider, model, tokens, duration, cost));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.emitAIRequest(provider, model, tokens, duration, cost);
|
|
334
|
+
}
|
|
335
|
+
emitAIRequest(provider, model, tokens, duration, cost) {
|
|
336
|
+
if (!this.aiRequestCounter) {
|
|
231
337
|
return;
|
|
232
338
|
}
|
|
233
339
|
const labels = { provider, model };
|
|
@@ -241,7 +347,17 @@ export class TelemetryService {
|
|
|
241
347
|
recordAIError(provider, error) {
|
|
242
348
|
// Track runtime metrics
|
|
243
349
|
this.errorCount++;
|
|
244
|
-
if (!this.enabled
|
|
350
|
+
if (!this.enabled) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!this.isReady) {
|
|
354
|
+
this.bufferPendingCall(() => this.emitAIError(provider, error));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.emitAIError(provider, error);
|
|
358
|
+
}
|
|
359
|
+
emitAIError(provider, error) {
|
|
360
|
+
if (!this.aiProviderErrors) {
|
|
245
361
|
return;
|
|
246
362
|
}
|
|
247
363
|
this.aiProviderErrors.add(1, {
|
|
@@ -251,7 +367,17 @@ export class TelemetryService {
|
|
|
251
367
|
});
|
|
252
368
|
}
|
|
253
369
|
recordMCPToolCall(toolName, duration, success) {
|
|
254
|
-
if (!this.enabled
|
|
370
|
+
if (!this.enabled) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!this.isReady) {
|
|
374
|
+
this.bufferPendingCall(() => this.emitMCPToolCall(toolName, duration, success));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
this.emitMCPToolCall(toolName, duration, success);
|
|
378
|
+
}
|
|
379
|
+
emitMCPToolCall(toolName, duration, success) {
|
|
380
|
+
if (!this.mcpToolCalls) {
|
|
255
381
|
return;
|
|
256
382
|
}
|
|
257
383
|
this.mcpToolCalls.add(1, {
|
|
@@ -263,7 +389,17 @@ export class TelemetryService {
|
|
|
263
389
|
recordConnection(type) {
|
|
264
390
|
// Track runtime metrics
|
|
265
391
|
this.activeConnectionCount++;
|
|
266
|
-
if (!this.enabled
|
|
392
|
+
if (!this.enabled) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!this.isReady) {
|
|
396
|
+
this.bufferPendingCall(() => this.emitConnection(type));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
this.emitConnection(type);
|
|
400
|
+
}
|
|
401
|
+
emitConnection(type) {
|
|
402
|
+
if (!this.connectionCounter) {
|
|
267
403
|
return;
|
|
268
404
|
}
|
|
269
405
|
this.connectionCounter.add(1, { connection_type: type });
|
|
@@ -271,7 +407,17 @@ export class TelemetryService {
|
|
|
271
407
|
recordConnectionClosed(type) {
|
|
272
408
|
// Track runtime metrics
|
|
273
409
|
this.activeConnectionCount = Math.max(0, this.activeConnectionCount - 1);
|
|
274
|
-
if (!this.enabled
|
|
410
|
+
if (!this.enabled) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!this.isReady) {
|
|
414
|
+
this.bufferPendingCall(() => this.emitConnectionClosed(type));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.emitConnectionClosed(type);
|
|
418
|
+
}
|
|
419
|
+
emitConnectionClosed(type) {
|
|
420
|
+
if (!this.connectionCounter) {
|
|
275
421
|
return;
|
|
276
422
|
}
|
|
277
423
|
// Optionally record disconnection metrics if needed
|
|
@@ -284,7 +430,17 @@ export class TelemetryService {
|
|
|
284
430
|
// Track runtime metrics
|
|
285
431
|
this.totalResponseTime += duration;
|
|
286
432
|
this.responseTimeCount++;
|
|
287
|
-
if (!this.enabled
|
|
433
|
+
if (!this.enabled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!this.isReady) {
|
|
437
|
+
this.bufferPendingCall(() => this.emitResponseTime(endpoint, method, duration));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this.emitResponseTime(endpoint, method, duration);
|
|
441
|
+
}
|
|
442
|
+
emitResponseTime(endpoint, method, duration) {
|
|
443
|
+
if (!this.responseTimeHistogram) {
|
|
288
444
|
return;
|
|
289
445
|
}
|
|
290
446
|
this.responseTimeHistogram.record(duration, {
|
|
@@ -295,7 +451,17 @@ export class TelemetryService {
|
|
|
295
451
|
}
|
|
296
452
|
// Custom Metrics
|
|
297
453
|
recordCustomMetric(name, value, labels) {
|
|
298
|
-
if (!this.enabled
|
|
454
|
+
if (!this.enabled) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (!this.isReady) {
|
|
458
|
+
this.bufferPendingCall(() => this.emitCustomMetric(name, value, labels));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
this.emitCustomMetric(name, value, labels);
|
|
462
|
+
}
|
|
463
|
+
emitCustomMetric(name, value, labels) {
|
|
464
|
+
if (!this.meter) {
|
|
299
465
|
return;
|
|
300
466
|
}
|
|
301
467
|
const counter = this.meter.createCounter(`custom_${name}`, {
|
|
@@ -304,7 +470,17 @@ export class TelemetryService {
|
|
|
304
470
|
counter.add(value, labels || {});
|
|
305
471
|
}
|
|
306
472
|
recordCustomHistogram(name, value, labels) {
|
|
307
|
-
if (!this.enabled
|
|
473
|
+
if (!this.enabled) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!this.isReady) {
|
|
477
|
+
this.bufferPendingCall(() => this.emitCustomHistogram(name, value, labels));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.emitCustomHistogram(name, value, labels);
|
|
481
|
+
}
|
|
482
|
+
emitCustomHistogram(name, value, labels) {
|
|
483
|
+
if (!this.meter) {
|
|
308
484
|
return;
|
|
309
485
|
}
|
|
310
486
|
const histogram = this.meter.createHistogram(`custom_${name}_histogram`, {
|
|
@@ -370,6 +546,16 @@ export class TelemetryService {
|
|
|
370
546
|
}
|
|
371
547
|
// Cleanup
|
|
372
548
|
async shutdown() {
|
|
549
|
+
// See initialize()'s comment: wait for the async instrument
|
|
550
|
+
// construction to settle before deciding whether there is a
|
|
551
|
+
// tracerProvider to shut down. Without this, a shutdown() called
|
|
552
|
+
// immediately after getInstance() would see no tracerProvider yet and
|
|
553
|
+
// no-op, while construction — which is already in flight — finishes
|
|
554
|
+
// moments later and leaves a live exporter (with its background flush
|
|
555
|
+
// timers) behind with nothing left to ever shut it down.
|
|
556
|
+
if (this.readyPromise) {
|
|
557
|
+
await this.readyPromise;
|
|
558
|
+
}
|
|
373
559
|
if (this.enabled &&
|
|
374
560
|
this.tracerProvider &&
|
|
375
561
|
!this.usingExternalTracerProvider) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export * from "./providerCatalog.js";
|
|
|
55
55
|
export * from "./providerCatalog.generated.js";
|
|
56
56
|
export * from "./providers.js";
|
|
57
57
|
export * from "./proxy.js";
|
|
58
|
+
export * from "./proxyIR.js";
|
|
58
59
|
export * from "./proxyClient.js";
|
|
59
60
|
export * from "./proxyContext.js";
|
|
60
61
|
export * from "./rag.js";
|
package/dist/types/index.js
CHANGED
|
@@ -59,6 +59,7 @@ export * from "./providerCatalog.js";
|
|
|
59
59
|
export * from "./providerCatalog.generated.js";
|
|
60
60
|
export * from "./providers.js";
|
|
61
61
|
export * from "./proxy.js";
|
|
62
|
+
export * from "./proxyIR.js";
|
|
62
63
|
export * from "./proxyClient.js";
|
|
63
64
|
export * from "./proxyContext.js";
|
|
64
65
|
export * from "./rag.js";
|