@juspay/neurolink 12.59.0 → 13.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/CHANGELOG.md +13 -2
- package/dist/auth/anthropicOAuth.js +3 -3
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/auth/sessionManager.js +4 -4
- package/dist/auth/tokenStore.js +4 -4
- package/dist/browser/neurolink.min.js +474 -473
- package/dist/cli/commands/observability.d.ts +1 -6
- package/dist/cli/commands/observability.js +0 -106
- package/dist/cli/commands/proxy.js +9 -18
- package/dist/cli/commands/telemetry.d.ts +3 -13
- package/dist/cli/commands/telemetry.js +7 -354
- package/dist/cli/loop/session.js +1 -1
- package/dist/client/httpClient.js +1 -1
- package/dist/client/sseClient.js +1 -1
- package/dist/client/streamingClient.js +2 -2
- package/dist/client/wsClient.js +1 -1
- package/dist/constants/langfuse.d.ts +25 -0
- package/dist/constants/langfuse.js +38 -0
- package/dist/constants/telemetry.d.ts +20 -0
- package/dist/constants/telemetry.js +34 -0
- package/dist/core/baseProvider.d.ts +2 -1
- package/dist/core/baseProvider.js +4 -11
- package/dist/core/conversationMemoryManager.js +5 -8
- package/dist/core/factory.js +1 -1
- package/dist/core/geminiLoopAdapter.d.ts +8 -1
- package/dist/core/geminiLoopAdapter.js +25 -1
- package/dist/core/loopEngine.js +69 -40
- package/dist/core/modules/MessageBuilder.js +2 -2
- package/dist/core/modules/StreamHandler.js +2 -2
- package/dist/core/modules/TelemetryHandler.d.ts +2 -18
- package/dist/core/modules/TelemetryHandler.js +1 -66
- package/dist/core/modules/ToolsManager.js +40 -31
- package/dist/core/nativeGenerateLoop.js +78 -8
- package/dist/core/redisConversationMemoryManager.js +8 -15
- package/dist/core/toolExecutionGuards.js +1 -13
- package/dist/index.d.ts +5 -12
- package/dist/index.js +5 -37
- package/dist/mcp/batching/requestBatcher.js +1 -1
- package/dist/mcp/externalServerManager.js +3 -21
- package/dist/mcp/toolDiscoveryService.js +4 -16
- package/dist/mcp/toolRegistry.js +2 -5
- package/dist/memory/memoryRetrievalTools.js +1 -1
- package/dist/neurolink.d.ts +16 -27
- package/dist/neurolink.js +788 -792
- package/dist/observability/exporterSpanProcessor.d.ts +50 -0
- package/dist/observability/exporterSpanProcessor.js +181 -0
- package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
- package/dist/observability/exporters/langfuseExporter.js +101 -154
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/langfuseMasking.d.ts +8 -0
- package/dist/observability/langfuseMasking.js +47 -0
- package/dist/observability/otlpEncoding.d.ts +20 -0
- package/dist/observability/otlpEncoding.js +85 -0
- package/dist/providers/amazonBedrock/client.js +142 -176
- package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
- package/dist/providers/amazonSagemaker.js +118 -80
- package/dist/providers/anthropic/client.js +3 -52
- package/dist/providers/anthropic/loopAdapter.js +24 -1
- package/dist/providers/anthropic/rateLimitCapture.js +9 -12
- package/dist/providers/googleAiStudio/client.d.ts +4 -5
- package/dist/providers/googleAiStudio/client.js +38 -40
- package/dist/providers/googleVertex/client.d.ts +2 -7
- package/dist/providers/googleVertex/client.js +282 -378
- package/dist/providers/googleVertex/constants.js +2 -2
- package/dist/providers/litellm/client.d.ts +2 -9
- package/dist/providers/litellm/client.js +1 -63
- package/dist/providers/openAI/client.d.ts +1 -8
- package/dist/providers/openAI/client.js +0 -65
- package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
- package/dist/providers/openaiChatCompletionsBase.js +61 -4
- package/dist/providers/replicate.js +22 -4
- package/dist/proxy/codexUsage.js +7 -0
- package/dist/proxy/proxyFetch.js +19 -25
- package/dist/proxy/proxyTracer.d.ts +14 -7
- package/dist/proxy/proxyTracer.js +129 -83
- package/dist/proxy/proxyTranslationEngine.js +0 -1
- package/dist/proxy/requestLogger.d.ts +1 -1
- package/dist/proxy/requestLogger.js +39 -45
- package/dist/server/middleware/common.js +1 -1
- package/dist/server/routes/agentRoutes.js +4 -4
- package/dist/server/routes/claudeProxyRoutes.js +99 -196
- package/dist/server/routes/codexProxyRoutes.js +621 -591
- package/dist/server/routes/healthRoutes.js +6 -6
- package/dist/server/routes/mcpRoutes.js +7 -7
- package/dist/server/routes/memoryRoutes.js +7 -7
- package/dist/server/routes/openApiRoutes.js +3 -3
- package/dist/server/routes/toolRoutes.js +5 -5
- package/dist/server/routes/transcribeRoutes.js +1 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
- package/dist/services/server/ai/observability/instrumentation.js +220 -1267
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
- package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
- package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
- package/dist/services/server/ai/observability/otlpSetup.js +58 -0
- package/dist/session/globalSessionState.js +0 -11
- package/dist/telemetry/attributes.d.ts +23 -46
- package/dist/telemetry/attributes.js +76 -162
- package/dist/telemetry/chatMessages.d.ts +13 -0
- package/dist/telemetry/chatMessages.js +59 -0
- package/dist/telemetry/index.d.ts +5 -19
- package/dist/telemetry/index.js +5 -26
- package/dist/telemetry/observation.d.ts +37 -0
- package/dist/telemetry/observation.js +112 -0
- package/dist/telemetry/observationNames.d.ts +18 -0
- package/dist/telemetry/observationNames.js +24 -0
- package/dist/telemetry/spanSanitizer.d.ts +39 -0
- package/dist/telemetry/spanSanitizer.js +92 -0
- package/dist/telemetry/spanSerialization.d.ts +13 -0
- package/dist/telemetry/spanSerialization.js +46 -0
- package/dist/telemetry/tracers.d.ts +37 -18
- package/dist/telemetry/tracers.js +97 -19
- package/dist/telemetry/withSpan.d.ts +47 -25
- package/dist/telemetry/withSpan.js +158 -71
- package/dist/types/cli.d.ts +0 -11
- package/dist/types/decision.d.ts +3 -0
- package/dist/types/exporter.d.ts +54 -0
- package/dist/types/exporter.js +0 -4
- package/dist/types/generate.d.ts +4 -1
- package/dist/types/isolatedAgent.d.ts +5 -4
- package/dist/types/loopEngine.d.ts +18 -28
- package/dist/types/observability.d.ts +127 -160
- package/dist/types/providers.d.ts +15 -0
- package/dist/types/proxy.d.ts +41 -2
- package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
- package/dist/utils/anthropicTraceSanitizer.js +3 -1
- package/dist/utils/conversationMemory.js +4 -4
- package/dist/utils/fileDetector.js +1 -1
- package/dist/utils/messageBuilder.js +1 -1
- package/dist/utils/noOutputSentinel.d.ts +4 -16
- package/dist/utils/noOutputSentinel.js +12 -28
- package/dist/utils/providerRetry.js +4 -4
- package/dist/voice/livekit/brain.js +1 -1
- package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
- package/dist/voice/livekit/realtimeMemory.js +66 -1
- package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
- package/docs-site/static/search-index.json +41 -63
- package/package.json +5 -4
- package/dist/telemetry/telemetryService.d.ts +0 -79
- package/dist/telemetry/telemetryService.js +0 -572
- package/dist/utils/observabilityHelpers.d.ts +0 -27
- package/dist/utils/observabilityHelpers.js +0 -47
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
import { context, metrics, trace, } from "@opentelemetry/api";
|
|
2
|
-
import { logger } from "../utils/logger.js";
|
|
3
|
-
export class TelemetryService {
|
|
4
|
-
static instance;
|
|
5
|
-
tracerProvider;
|
|
6
|
-
enabled = false;
|
|
7
|
-
initialized = false;
|
|
8
|
-
usingExternalTracerProvider = false;
|
|
9
|
-
meter;
|
|
10
|
-
tracer;
|
|
11
|
-
// Optional Metrics (only created when enabled)
|
|
12
|
-
aiRequestCounter;
|
|
13
|
-
aiRequestDuration;
|
|
14
|
-
aiTokensUsed;
|
|
15
|
-
aiProviderErrors;
|
|
16
|
-
aiCostUsd;
|
|
17
|
-
mcpToolCalls;
|
|
18
|
-
connectionCounter;
|
|
19
|
-
responseTimeHistogram;
|
|
20
|
-
// Runtime metrics tracking
|
|
21
|
-
activeConnectionCount = 0;
|
|
22
|
-
errorCount = 0;
|
|
23
|
-
requestCount = 0;
|
|
24
|
-
totalResponseTime = 0;
|
|
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;
|
|
44
|
-
constructor() {
|
|
45
|
-
// Check if telemetry is enabled
|
|
46
|
-
this.enabled = this.isTelemetryEnabled();
|
|
47
|
-
if (this.enabled) {
|
|
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();
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
logger.debug("[Telemetry] Disabled - set NEUROLINK_TELEMETRY_ENABLED=true or configure OTEL_EXPORTER_OTLP_ENDPOINT to enable");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
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.
|
|
72
|
-
if (!TelemetryService.instance) {
|
|
73
|
-
TelemetryService.instance = new TelemetryService();
|
|
74
|
-
}
|
|
75
|
-
return TelemetryService.instance;
|
|
76
|
-
}
|
|
77
|
-
isTelemetryEnabled() {
|
|
78
|
-
return (this.hasExternalTracerProvider() ||
|
|
79
|
-
process.env.NEUROLINK_TELEMETRY_ENABLED === "true" ||
|
|
80
|
-
process.env.OTEL_EXPORTER_OTLP_ENDPOINT !== undefined);
|
|
81
|
-
}
|
|
82
|
-
hasExternalTracerProvider() {
|
|
83
|
-
try {
|
|
84
|
-
const provider = trace.getTracerProvider();
|
|
85
|
-
if (!provider) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
const providerName = provider.constructor?.name || "";
|
|
89
|
-
if (providerName &&
|
|
90
|
-
providerName !== "ProxyTracerProvider" &&
|
|
91
|
-
providerName !== "NoopTracerProvider") {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
const delegate = typeof provider.getDelegate === "function"
|
|
95
|
-
? provider.getDelegate()
|
|
96
|
-
: provider._delegate;
|
|
97
|
-
const delegateName = delegate?.constructor?.name || "";
|
|
98
|
-
return Boolean(delegateName && delegateName !== "NoopTracerProvider");
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
logger.warn("[Telemetry] Failed checking for external TracerProvider", {
|
|
102
|
-
error: error instanceof Error ? error.message : String(error),
|
|
103
|
-
});
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
adoptExternalTracerProvider(reason) {
|
|
108
|
-
this.usingExternalTracerProvider = true;
|
|
109
|
-
this.tracerProvider = undefined;
|
|
110
|
-
this.meter = metrics.getMeter("neurolink-ai");
|
|
111
|
-
this.tracer = trace.getTracer("neurolink-ai");
|
|
112
|
-
this.initializeMetrics();
|
|
113
|
-
logger.debug("[Telemetry] Reusing externally managed TracerProvider", {
|
|
114
|
-
reason,
|
|
115
|
-
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
async initializeTelemetry() {
|
|
119
|
-
try {
|
|
120
|
-
if (this.hasExternalTracerProvider()) {
|
|
121
|
-
this.adoptExternalTracerProvider("global tracer provider already registered");
|
|
122
|
-
this.markReady();
|
|
123
|
-
return;
|
|
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
|
-
]);
|
|
131
|
-
const resource = resourceFromAttributes({
|
|
132
|
-
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "neurolink-ai",
|
|
133
|
-
[ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || "3.0.1",
|
|
134
|
-
});
|
|
135
|
-
const exporter = new OTLPTraceExporter({
|
|
136
|
-
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
137
|
-
? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`
|
|
138
|
-
: undefined,
|
|
139
|
-
});
|
|
140
|
-
this.tracerProvider = new BasicTracerProvider({
|
|
141
|
-
resource,
|
|
142
|
-
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
143
|
-
});
|
|
144
|
-
this.meter = metrics.getMeter("neurolink-ai");
|
|
145
|
-
this.tracer = this.tracerProvider.getTracer("neurolink-ai");
|
|
146
|
-
this.initializeMetrics();
|
|
147
|
-
this.markReady();
|
|
148
|
-
logger.debug("[Telemetry] Initialized local telemetry exporter", {
|
|
149
|
-
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
150
|
-
globalTracerProviderOwnedBy: "observability/instrumentation",
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
155
|
-
const isDuplicateRegistration = errorMessage.includes("duplicate registration") ||
|
|
156
|
-
errorMessage.includes("already registered") ||
|
|
157
|
-
errorMessage.includes("already set");
|
|
158
|
-
if (isDuplicateRegistration && this.hasExternalTracerProvider()) {
|
|
159
|
-
this.adoptExternalTracerProvider("duplicate global tracer registration detected");
|
|
160
|
-
this.markReady();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
logger.error("[Telemetry] Failed to initialize:", error);
|
|
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();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
initializeMetrics() {
|
|
173
|
-
if (!this.enabled || !this.meter) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
this.aiRequestCounter = this.meter.createCounter("ai_requests_total", {
|
|
177
|
-
description: "Total number of AI requests",
|
|
178
|
-
});
|
|
179
|
-
this.aiRequestDuration = this.meter.createHistogram("ai_request_duration_ms", {
|
|
180
|
-
description: "AI request duration in milliseconds",
|
|
181
|
-
});
|
|
182
|
-
this.aiTokensUsed = this.meter.createCounter("ai_tokens_used_total", {
|
|
183
|
-
description: "Total number of AI tokens used",
|
|
184
|
-
});
|
|
185
|
-
this.aiCostUsd = this.meter.createCounter("ai_cost_usd_total", {
|
|
186
|
-
description: "Total accumulated AI cost in USD",
|
|
187
|
-
});
|
|
188
|
-
this.aiProviderErrors = this.meter.createCounter("ai_provider_errors_total", {
|
|
189
|
-
description: "Total number of AI provider errors",
|
|
190
|
-
});
|
|
191
|
-
this.mcpToolCalls = this.meter.createCounter("mcp_tool_calls_total", {
|
|
192
|
-
description: "Total number of MCP tool calls",
|
|
193
|
-
});
|
|
194
|
-
this.connectionCounter = this.meter.createCounter("connections_total", {
|
|
195
|
-
description: "Total number of connections",
|
|
196
|
-
});
|
|
197
|
-
this.responseTimeHistogram = this.meter.createHistogram("response_time_ms", {
|
|
198
|
-
description: "Response time in milliseconds",
|
|
199
|
-
});
|
|
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
|
-
}
|
|
237
|
-
async initialize() {
|
|
238
|
-
if (!this.enabled) {
|
|
239
|
-
return;
|
|
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
|
-
}
|
|
252
|
-
if (this.usingExternalTracerProvider) {
|
|
253
|
-
this.initialized = true;
|
|
254
|
-
logger.debug("[Telemetry] External TracerProvider already initialized by host");
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (!this.tracerProvider) {
|
|
258
|
-
this.initialized = true;
|
|
259
|
-
logger.debug("[Telemetry] Tracer provider already prepared during constructor");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
// Register AsyncLocalStorage context manager for proper parent-child
|
|
264
|
-
// span relationships across async boundaries (required for startActiveSpan)
|
|
265
|
-
try {
|
|
266
|
-
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks");
|
|
267
|
-
context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// context-async-hooks not installed — context propagation
|
|
271
|
-
// will use the default (noop) manager
|
|
272
|
-
}
|
|
273
|
-
this.initialized = true;
|
|
274
|
-
logger.debug("[Telemetry] Tracer provider started successfully");
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
logger.error("[Telemetry] Failed to start:", error);
|
|
278
|
-
this.enabled = false;
|
|
279
|
-
this.initialized = false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// AI Operation Tracing (NO-OP when disabled)
|
|
283
|
-
/**
|
|
284
|
-
* @deprecated Vercel AI SDK's experimental_telemetry creates ai.generateText/ai.streamText
|
|
285
|
-
* spans automatically via OpenTelemetry. Using this method would create duplicate spans.
|
|
286
|
-
* Kept for potential future use with non-Vercel providers (e.g., Amazon Bedrock).
|
|
287
|
-
* See: TelemetryHandler.getTelemetryConfig() for the active telemetry path.
|
|
288
|
-
*/
|
|
289
|
-
async traceAIRequest(provider, operation, operationType = "generate_text") {
|
|
290
|
-
if (!this.enabled || !this.tracer) {
|
|
291
|
-
return await operation();
|
|
292
|
-
}
|
|
293
|
-
const span = this.tracer.startSpan(`ai.${provider}.${operationType}`, {
|
|
294
|
-
attributes: {
|
|
295
|
-
"ai.provider": provider,
|
|
296
|
-
"ai.operation": operationType,
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
try {
|
|
300
|
-
const result = await operation();
|
|
301
|
-
span.setStatus({ code: 1 }); // OK
|
|
302
|
-
return result;
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
span.setStatus({
|
|
306
|
-
code: 2,
|
|
307
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
308
|
-
}); // ERROR
|
|
309
|
-
span.recordException(error);
|
|
310
|
-
throw error;
|
|
311
|
-
}
|
|
312
|
-
finally {
|
|
313
|
-
span.end();
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Metrics Recording (NO-OP when disabled)
|
|
317
|
-
recordAIRequest(provider, model, tokens, duration, cost) {
|
|
318
|
-
// Track runtime metrics
|
|
319
|
-
this.requestCount++;
|
|
320
|
-
this.totalResponseTime += duration;
|
|
321
|
-
this.responseTimeCount++;
|
|
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) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const labels = { provider, model };
|
|
340
|
-
this.aiRequestCounter.add(1, labels);
|
|
341
|
-
this.aiRequestDuration?.record(duration, labels);
|
|
342
|
-
this.aiTokensUsed?.add(tokens, labels);
|
|
343
|
-
if (cost !== undefined && Number.isFinite(cost) && cost > 0) {
|
|
344
|
-
this.aiCostUsd?.add(cost, labels);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
recordAIError(provider, error) {
|
|
348
|
-
// Track runtime metrics
|
|
349
|
-
this.errorCount++;
|
|
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) {
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
this.aiProviderErrors.add(1, {
|
|
364
|
-
provider,
|
|
365
|
-
error: error.name,
|
|
366
|
-
message: error.message.substring(0, 100), // Limit message length
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
recordMCPToolCall(toolName, duration, success) {
|
|
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) {
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
this.mcpToolCalls.add(1, {
|
|
384
|
-
tool: toolName,
|
|
385
|
-
success: success.toString(),
|
|
386
|
-
duration_bucket: this.getDurationBucket(duration),
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
recordConnection(type) {
|
|
390
|
-
// Track runtime metrics
|
|
391
|
-
this.activeConnectionCount++;
|
|
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) {
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
this.connectionCounter.add(1, { connection_type: type });
|
|
406
|
-
}
|
|
407
|
-
recordConnectionClosed(type) {
|
|
408
|
-
// Track runtime metrics
|
|
409
|
-
this.activeConnectionCount = Math.max(0, this.activeConnectionCount - 1);
|
|
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) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
// Optionally record disconnection metrics if needed
|
|
424
|
-
this.connectionCounter.add(-1, {
|
|
425
|
-
connection_type: type,
|
|
426
|
-
event: "disconnect",
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
recordResponseTime(endpoint, method, duration) {
|
|
430
|
-
// Track runtime metrics
|
|
431
|
-
this.totalResponseTime += duration;
|
|
432
|
-
this.responseTimeCount++;
|
|
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) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
this.responseTimeHistogram.record(duration, {
|
|
447
|
-
endpoint,
|
|
448
|
-
method,
|
|
449
|
-
status_bucket: this.getStatusBucket(duration),
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
// Custom Metrics
|
|
453
|
-
recordCustomMetric(name, value, labels) {
|
|
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) {
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
const counter = this.meter.createCounter(`custom_${name}`, {
|
|
468
|
-
description: `Custom metric: ${name}`,
|
|
469
|
-
});
|
|
470
|
-
counter.add(value, labels || {});
|
|
471
|
-
}
|
|
472
|
-
recordCustomHistogram(name, value, labels) {
|
|
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) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const histogram = this.meter.createHistogram(`custom_${name}_histogram`, {
|
|
487
|
-
description: `Custom histogram: ${name}`,
|
|
488
|
-
});
|
|
489
|
-
histogram.record(value, labels || {});
|
|
490
|
-
}
|
|
491
|
-
// Health Checks
|
|
492
|
-
async getHealthMetrics() {
|
|
493
|
-
const memoryUsage = process.memoryUsage();
|
|
494
|
-
// Calculate error rate as percentage of errors vs total requests
|
|
495
|
-
const errorRate = this.requestCount > 0 ? (this.errorCount / this.requestCount) * 100 : 0;
|
|
496
|
-
// Calculate average response time
|
|
497
|
-
const averageResponseTime = this.responseTimeCount > 0
|
|
498
|
-
? this.totalResponseTime / this.responseTimeCount
|
|
499
|
-
: 0;
|
|
500
|
-
return {
|
|
501
|
-
timestamp: Date.now(),
|
|
502
|
-
memoryUsage,
|
|
503
|
-
uptime: process.uptime(),
|
|
504
|
-
activeConnections: this.activeConnectionCount,
|
|
505
|
-
errorRate: Math.round(errorRate * 100) / 100, // Round to 2 decimal places
|
|
506
|
-
averageResponseTime: Math.round(averageResponseTime * 100) / 100, // Round to 2 decimal places
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
// Telemetry Status
|
|
510
|
-
isEnabled() {
|
|
511
|
-
return this.enabled;
|
|
512
|
-
}
|
|
513
|
-
getStatus() {
|
|
514
|
-
return {
|
|
515
|
-
enabled: this.enabled,
|
|
516
|
-
initialized: this.initialized,
|
|
517
|
-
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
518
|
-
service: process.env.OTEL_SERVICE_NAME || "neurolink-ai",
|
|
519
|
-
version: process.env.OTEL_SERVICE_VERSION || "3.0.1",
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
// Helper methods
|
|
523
|
-
getDurationBucket(duration) {
|
|
524
|
-
if (duration < 100) {
|
|
525
|
-
return "fast";
|
|
526
|
-
}
|
|
527
|
-
if (duration < 500) {
|
|
528
|
-
return "medium";
|
|
529
|
-
}
|
|
530
|
-
if (duration < 1000) {
|
|
531
|
-
return "slow";
|
|
532
|
-
}
|
|
533
|
-
return "very_slow";
|
|
534
|
-
}
|
|
535
|
-
getStatusBucket(duration) {
|
|
536
|
-
if (duration < 200) {
|
|
537
|
-
return "excellent";
|
|
538
|
-
}
|
|
539
|
-
if (duration < 500) {
|
|
540
|
-
return "good";
|
|
541
|
-
}
|
|
542
|
-
if (duration < 1000) {
|
|
543
|
-
return "acceptable";
|
|
544
|
-
}
|
|
545
|
-
return "poor";
|
|
546
|
-
}
|
|
547
|
-
// Cleanup
|
|
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
|
-
}
|
|
559
|
-
if (this.enabled &&
|
|
560
|
-
this.tracerProvider &&
|
|
561
|
-
!this.usingExternalTracerProvider) {
|
|
562
|
-
try {
|
|
563
|
-
await this.tracerProvider.shutdown();
|
|
564
|
-
this.initialized = false;
|
|
565
|
-
logger.debug("[Telemetry] Tracer provider shutdown completed");
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
logger.error("[Telemetry] Error during shutdown:", error);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility for building observability configs from environment variables
|
|
3
|
-
*/
|
|
4
|
-
import type { ObservabilityConfig } from "../types/index.js";
|
|
5
|
-
/**
|
|
6
|
-
* Build observability config from environment variables
|
|
7
|
-
*
|
|
8
|
-
* Reads Langfuse configuration from environment:
|
|
9
|
-
* - LANGFUSE_ENABLED: Enable/disable Langfuse (must be "true")
|
|
10
|
-
* - LANGFUSE_PUBLIC_KEY: Your Langfuse public key (required)
|
|
11
|
-
* - LANGFUSE_SECRET_KEY: Your Langfuse secret key (required)
|
|
12
|
-
* - LANGFUSE_BASE_URL: Langfuse server URL (default: https://cloud.langfuse.com)
|
|
13
|
-
* - LANGFUSE_ENVIRONMENT: Environment name (default: dev)
|
|
14
|
-
* - PUBLIC_APP_VERSION: Release/version identifier (default: v1.0.0)
|
|
15
|
-
*
|
|
16
|
-
* @returns ObservabilityConfig if all required env vars are set, undefined otherwise
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* import { NeuroLink, buildObservabilityConfigFromEnv } from '@juspay/neurolink';
|
|
21
|
-
*
|
|
22
|
-
* const neurolink = new NeuroLink({
|
|
23
|
-
* observability: buildObservabilityConfigFromEnv()
|
|
24
|
-
* });
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export declare function buildObservabilityConfigFromEnv(): ObservabilityConfig | undefined;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility for building observability configs from environment variables
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Build observability config from environment variables
|
|
6
|
-
*
|
|
7
|
-
* Reads Langfuse configuration from environment:
|
|
8
|
-
* - LANGFUSE_ENABLED: Enable/disable Langfuse (must be "true")
|
|
9
|
-
* - LANGFUSE_PUBLIC_KEY: Your Langfuse public key (required)
|
|
10
|
-
* - LANGFUSE_SECRET_KEY: Your Langfuse secret key (required)
|
|
11
|
-
* - LANGFUSE_BASE_URL: Langfuse server URL (default: https://cloud.langfuse.com)
|
|
12
|
-
* - LANGFUSE_ENVIRONMENT: Environment name (default: dev)
|
|
13
|
-
* - PUBLIC_APP_VERSION: Release/version identifier (default: v1.0.0)
|
|
14
|
-
*
|
|
15
|
-
* @returns ObservabilityConfig if all required env vars are set, undefined otherwise
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* import { NeuroLink, buildObservabilityConfigFromEnv } from '@juspay/neurolink';
|
|
20
|
-
*
|
|
21
|
-
* const neurolink = new NeuroLink({
|
|
22
|
-
* observability: buildObservabilityConfigFromEnv()
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export function buildObservabilityConfigFromEnv() {
|
|
27
|
-
const langfuseEnabled = process.env.LANGFUSE_ENABLED?.trim().toLowerCase() === "true";
|
|
28
|
-
const publicKey = process.env.LANGFUSE_PUBLIC_KEY?.trim();
|
|
29
|
-
const secretKey = process.env.LANGFUSE_SECRET_KEY?.trim();
|
|
30
|
-
if (!langfuseEnabled || !publicKey || !secretKey) {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
langfuse: {
|
|
35
|
-
enabled: langfuseEnabled,
|
|
36
|
-
publicKey,
|
|
37
|
-
secretKey,
|
|
38
|
-
baseUrl: process.env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com",
|
|
39
|
-
environment: process.env.LANGFUSE_ENVIRONMENT?.trim() ||
|
|
40
|
-
process.env.PUBLIC_APP_ENVIRONMENT?.trim() ||
|
|
41
|
-
"dev",
|
|
42
|
-
release: process.env.PUBLIC_APP_VERSION?.trim() ||
|
|
43
|
-
process.env.npm_package_version?.trim() ||
|
|
44
|
-
"v1.0.0",
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|