@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
|
@@ -6,22 +6,20 @@
|
|
|
6
6
|
* receive -> account_selection -> upstream (per retry) -> stream -> end
|
|
7
7
|
*
|
|
8
8
|
* Uses the existing instrumentation infrastructure:
|
|
9
|
-
* - getTracer() from instrumentation.ts for span creation
|
|
10
9
|
* - setLangfuseContext() for Langfuse enrichment
|
|
11
|
-
* -
|
|
10
|
+
* - OTel propagation API for context propagation to/from upstream
|
|
12
11
|
* - SpanAttributes from spanTypes.ts for attribute naming
|
|
13
12
|
* - calculateCost() from pricing.ts for cost tracking
|
|
14
|
-
* - TelemetryService for metrics recording
|
|
15
13
|
*/
|
|
16
14
|
import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { setLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
|
|
16
|
+
import { spanContentAttribute } from "../telemetry/attributes.js";
|
|
17
|
+
import { observationLevel, observe, observeTrace, usageDetails, } from "../telemetry/observation.js";
|
|
18
|
+
import { generationName } from "../telemetry/observationNames.js";
|
|
19
19
|
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
20
20
|
import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
|
|
21
|
-
import { TelemetryService } from "../telemetry/telemetryService.js";
|
|
22
21
|
import { logger } from "../utils/logger.js";
|
|
23
22
|
import { registerProxyRequestTraceContext, releaseProxyRequestTraceContext, } from "./proxyTraceContext.js";
|
|
24
|
-
const LOG_PREFIX = "[ProxyTracer]";
|
|
25
23
|
// ---------------------------------------------------------------------------
|
|
26
24
|
// OTEL Metric Instruments — lazy singleton
|
|
27
25
|
//
|
|
@@ -36,7 +34,7 @@ function getProxyMetrics() {
|
|
|
36
34
|
if (_metrics) {
|
|
37
35
|
return _metrics;
|
|
38
36
|
}
|
|
39
|
-
const meter = metrics.getMeter("
|
|
37
|
+
const meter = metrics.getMeter("proxy", "1.0.0");
|
|
40
38
|
const createdMetrics = {
|
|
41
39
|
requestsTotal: meter.createCounter("proxy_requests_total", {
|
|
42
40
|
description: "Total number of proxy requests",
|
|
@@ -178,6 +176,14 @@ function buildBodyEventAttributes(body) {
|
|
|
178
176
|
"proxy.body.truncated": body.length > MAX_BODY_LOG_SIZE,
|
|
179
177
|
};
|
|
180
178
|
}
|
|
179
|
+
function parseBody(body) {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(body);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return body;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
181
187
|
// ---------------------------------------------------------------------------
|
|
182
188
|
// Client app detection
|
|
183
189
|
// ---------------------------------------------------------------------------
|
|
@@ -198,8 +204,8 @@ function detectClientApp(userAgent) {
|
|
|
198
204
|
// ---------------------------------------------------------------------------
|
|
199
205
|
class ProxyTracer {
|
|
200
206
|
rootSpan;
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
traceContext;
|
|
208
|
+
proxyTracer = trace.getTracer("proxy");
|
|
203
209
|
requestId;
|
|
204
210
|
/**
|
|
205
211
|
* Model used for costing. Starts as the model the client asked for and is
|
|
@@ -223,10 +229,21 @@ class ProxyTracer {
|
|
|
223
229
|
mode = "full";
|
|
224
230
|
/** Where the Claude-shaped request originated, for cache/cost attribution. */
|
|
225
231
|
requestOrigin = "native";
|
|
226
|
-
|
|
232
|
+
requestedModel;
|
|
233
|
+
input;
|
|
234
|
+
output;
|
|
235
|
+
responseModel;
|
|
236
|
+
finishReason;
|
|
237
|
+
firstOutputAt;
|
|
238
|
+
upstreamStartedAt;
|
|
239
|
+
cost;
|
|
240
|
+
statusMessage;
|
|
241
|
+
constructor(rootSpan, traceContext, requestId, model, stream, billingProvider) {
|
|
227
242
|
this.rootSpan = rootSpan;
|
|
243
|
+
this.traceContext = traceContext;
|
|
228
244
|
this.requestId = requestId;
|
|
229
245
|
this.model = model;
|
|
246
|
+
this.requestedModel = model;
|
|
230
247
|
this.billingProvider = billingProvider;
|
|
231
248
|
this.startTime = Date.now();
|
|
232
249
|
this.isStream = stream;
|
|
@@ -235,35 +252,47 @@ class ProxyTracer {
|
|
|
235
252
|
* Create a root span for a proxy request and set Langfuse context.
|
|
236
253
|
*
|
|
237
254
|
* If the incoming request carries a `traceparent` header, the root span
|
|
238
|
-
* will be linked to the caller's trace via
|
|
255
|
+
* will be linked to the caller's trace via the OTel propagation API.
|
|
239
256
|
*/
|
|
240
257
|
static startRequest(ctx, incomingHeaders) {
|
|
241
|
-
const tracer = getTracer("
|
|
258
|
+
const tracer = trace.getTracer("proxy");
|
|
242
259
|
// Extract parent context from incoming headers (Claude Code may send traceparent)
|
|
243
260
|
let parentContext = context.active();
|
|
244
261
|
if (incomingHeaders && !trace.getSpan(context.active())) {
|
|
245
|
-
const
|
|
246
|
-
|
|
262
|
+
const headers = { ...incomingHeaders };
|
|
263
|
+
if (headers["traceparent"]?.includes(", ")) {
|
|
264
|
+
headers["traceparent"] = headers["traceparent"].split(", ")[0];
|
|
265
|
+
}
|
|
266
|
+
const extracted = trace.getSpanContext(propagation.extract(context.active(), headers));
|
|
247
267
|
if (extracted) {
|
|
248
268
|
// Create a remote span context so the root span becomes a child of the caller
|
|
249
269
|
parentContext = trace.setSpanContext(context.active(), extracted);
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
const clientApp = ctx.clientApp ?? detectClientApp(ctx.userAgent);
|
|
273
|
+
const traceContext = context.with(parentContext, () => setLangfuseContext({
|
|
274
|
+
userId: incomingHeaders?.["x-neurolink-user-id"],
|
|
275
|
+
sessionId: incomingHeaders?.["x-neurolink-session-id"] ?? ctx.sessionId,
|
|
276
|
+
conversationId: incomingHeaders?.["x-neurolink-conversation-id"],
|
|
277
|
+
requestId: ctx.requestId,
|
|
278
|
+
traceName: `proxy:${ctx.model}`,
|
|
279
|
+
metadata: {
|
|
280
|
+
clientApp,
|
|
281
|
+
stream: ctx.stream,
|
|
282
|
+
toolCount: ctx.toolCount,
|
|
283
|
+
},
|
|
284
|
+
}, () => context.active()));
|
|
253
285
|
const rootSpan = tracer.startSpan("proxy.request", {
|
|
254
286
|
attributes: {
|
|
255
287
|
"proxy.request_id": ctx.requestId,
|
|
256
288
|
"http.method": ctx.method,
|
|
257
289
|
"http.target": ctx.path,
|
|
258
|
-
"
|
|
290
|
+
"proxy.request.model": ctx.model,
|
|
259
291
|
"proxy.stream": ctx.stream,
|
|
260
292
|
"proxy.tool_count": ctx.toolCount,
|
|
261
293
|
"proxy.client_app": clientApp,
|
|
262
294
|
},
|
|
263
|
-
},
|
|
264
|
-
if (ctx.sessionId) {
|
|
265
|
-
rootSpan.setAttribute("session.id", ctx.sessionId);
|
|
266
|
-
}
|
|
295
|
+
}, traceContext);
|
|
267
296
|
if (ctx.userAgent) {
|
|
268
297
|
rootSpan.setAttribute("http.user_agent", ctx.userAgent);
|
|
269
298
|
}
|
|
@@ -271,42 +300,10 @@ class ProxyTracer {
|
|
|
271
300
|
// What the caller exposed to the model (tool catalogue for this request).
|
|
272
301
|
rootSpan.setAttribute("gen_ai.request.tool_names", JSON.stringify(ctx.toolNames));
|
|
273
302
|
}
|
|
274
|
-
|
|
275
|
-
const nlSessionId = incomingHeaders?.["x-neurolink-session-id"];
|
|
276
|
-
const nlUserId = incomingHeaders?.["x-neurolink-user-id"];
|
|
277
|
-
const nlConversationId = incomingHeaders?.["x-neurolink-conversation-id"];
|
|
278
|
-
if (nlSessionId) {
|
|
279
|
-
rootSpan.setAttribute("neurolink.session_id", nlSessionId);
|
|
280
|
-
}
|
|
281
|
-
if (nlUserId) {
|
|
282
|
-
rootSpan.setAttribute("neurolink.user_id", nlUserId);
|
|
283
|
-
}
|
|
284
|
-
if (nlConversationId) {
|
|
285
|
-
rootSpan.setAttribute("neurolink.conversation_id", nlConversationId);
|
|
286
|
-
}
|
|
287
|
-
const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
|
|
303
|
+
const instance = new ProxyTracer(rootSpan, trace.setSpan(traceContext, rootSpan), ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
|
|
288
304
|
instance.recordRequestMetrics = ctx.recordRequestMetrics !== false;
|
|
289
305
|
instance.recordUsageMetrics =
|
|
290
306
|
ctx.recordUsageMetrics ?? instance.recordRequestMetrics;
|
|
291
|
-
// Set Langfuse context (fire-and-forget — non-blocking)
|
|
292
|
-
// Prefer NeuroLink session/user from calling SDK over Claude Code session
|
|
293
|
-
setLangfuseContext({
|
|
294
|
-
sessionId: nlSessionId ?? ctx.sessionId,
|
|
295
|
-
userId: nlUserId,
|
|
296
|
-
conversationId: nlConversationId,
|
|
297
|
-
requestId: ctx.requestId,
|
|
298
|
-
traceName: `proxy:${ctx.model}`,
|
|
299
|
-
operationName: "proxy.request",
|
|
300
|
-
metadata: {
|
|
301
|
-
clientApp,
|
|
302
|
-
stream: ctx.stream,
|
|
303
|
-
toolCount: ctx.toolCount,
|
|
304
|
-
},
|
|
305
|
-
}).catch((err) => {
|
|
306
|
-
logger.debug(`${LOG_PREFIX} Failed to set Langfuse context`, {
|
|
307
|
-
error: err instanceof Error ? err.message : String(err),
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
307
|
return instance;
|
|
311
308
|
}
|
|
312
309
|
// -------------------------------------------------------------------------
|
|
@@ -318,7 +315,7 @@ class ProxyTracer {
|
|
|
318
315
|
attributes: {
|
|
319
316
|
"proxy.request_id": this.requestId,
|
|
320
317
|
},
|
|
321
|
-
},
|
|
318
|
+
}, this.traceContext);
|
|
322
319
|
}
|
|
323
320
|
/** Span covering account selection logic (fill-first / round-robin). */
|
|
324
321
|
startAccountSelection() {
|
|
@@ -326,10 +323,11 @@ class ProxyTracer {
|
|
|
326
323
|
attributes: {
|
|
327
324
|
"proxy.request_id": this.requestId,
|
|
328
325
|
},
|
|
329
|
-
},
|
|
326
|
+
}, this.traceContext);
|
|
330
327
|
}
|
|
331
328
|
/** Span covering a single upstream attempt. One per retry. */
|
|
332
329
|
startUpstreamAttempt(ctx) {
|
|
330
|
+
this.upstreamStartedAt ??= Date.now();
|
|
333
331
|
return this.proxyTracer.startSpan("proxy.upstream", {
|
|
334
332
|
attributes: {
|
|
335
333
|
"proxy.request_id": this.requestId,
|
|
@@ -339,16 +337,16 @@ class ProxyTracer {
|
|
|
339
337
|
"proxy.upstream.polyfill_body": ctx.polyfillBody,
|
|
340
338
|
"http.url": ctx.upstreamUrl,
|
|
341
339
|
},
|
|
342
|
-
},
|
|
340
|
+
}, this.traceContext);
|
|
343
341
|
}
|
|
344
342
|
/** Span covering the SSE stream relay phase. */
|
|
345
343
|
startStream() {
|
|
346
344
|
return this.proxyTracer.startSpan("proxy.stream", {
|
|
347
345
|
attributes: {
|
|
348
346
|
"proxy.request_id": this.requestId,
|
|
349
|
-
"
|
|
347
|
+
"proxy.request.model": this.model,
|
|
350
348
|
},
|
|
351
|
-
},
|
|
349
|
+
}, this.traceContext);
|
|
352
350
|
}
|
|
353
351
|
// -------------------------------------------------------------------------
|
|
354
352
|
// Attribute setters
|
|
@@ -369,10 +367,6 @@ class ProxyTracer {
|
|
|
369
367
|
if (ctx.rateLimitBefore7d !== undefined) {
|
|
370
368
|
this.rootSpan.setAttribute("proxy.ratelimit.before.7d", ctx.rateLimitBefore7d);
|
|
371
369
|
}
|
|
372
|
-
// Update Langfuse context with account as userId
|
|
373
|
-
setLangfuseContext({ userId: ctx.selectedAccount }).catch(() => {
|
|
374
|
-
// Non-fatal
|
|
375
|
-
});
|
|
376
370
|
}
|
|
377
371
|
/** Attribute fallback metrics to the account that actually served them. */
|
|
378
372
|
setServedAccount(account, accountType) {
|
|
@@ -416,11 +410,10 @@ class ProxyTracer {
|
|
|
416
410
|
if (ctx.reasoningTokens !== undefined) {
|
|
417
411
|
this.rootSpan.setAttribute("ai.tokens.reasoning", ctx.reasoningTokens);
|
|
418
412
|
}
|
|
419
|
-
// GenAI semantic convention attributes (for Langfuse compatibility)
|
|
420
413
|
this.rootSpan.setAttributes({
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"
|
|
414
|
+
"proxy.usage.input_tokens": totalTokens - ctx.outputTokens,
|
|
415
|
+
"proxy.usage.output_tokens": ctx.outputTokens,
|
|
416
|
+
"proxy.usage.total_tokens": totalTokens,
|
|
424
417
|
});
|
|
425
418
|
// Prefix-inferred prices are not an observed model rate. Keep provenance,
|
|
426
419
|
// but emit an amount only when this model has an exact price-table entry.
|
|
@@ -435,6 +428,7 @@ class ProxyTracer {
|
|
|
435
428
|
});
|
|
436
429
|
if (pricingStatus === "exact") {
|
|
437
430
|
const cost = calculateCost(this.billingProvider, this.model, billingUsage);
|
|
431
|
+
this.cost = cost;
|
|
438
432
|
this.rootSpan.setAttributes({
|
|
439
433
|
"ai.cost.total": cost,
|
|
440
434
|
"ai.cost.currency": "USD",
|
|
@@ -454,8 +448,14 @@ class ProxyTracer {
|
|
|
454
448
|
* invoked (tool_use blocks). Uses gen_ai.* semantic-convention attributes.
|
|
455
449
|
*/
|
|
456
450
|
setResponseInfo(ctx) {
|
|
451
|
+
this.responseModel = ctx.responseModel ?? this.responseModel;
|
|
452
|
+
this.finishReason = ctx.finishReason ?? this.finishReason;
|
|
453
|
+
this.firstOutputAt = ctx.firstOutputAt ?? this.firstOutputAt;
|
|
454
|
+
if (BODY_LOGGING_ENABLED && ctx.output !== undefined) {
|
|
455
|
+
this.output = ctx.output;
|
|
456
|
+
}
|
|
457
457
|
if (ctx.responseModel) {
|
|
458
|
-
this.rootSpan.setAttribute("
|
|
458
|
+
this.rootSpan.setAttribute("proxy.response.model", ctx.responseModel);
|
|
459
459
|
}
|
|
460
460
|
if (ctx.finishReason) {
|
|
461
461
|
this.rootSpan.setAttribute("gen_ai.response.finish_reason", ctx.finishReason);
|
|
@@ -475,6 +475,7 @@ class ProxyTracer {
|
|
|
475
475
|
}
|
|
476
476
|
/** Record an error on the root span. */
|
|
477
477
|
setError(errorType, errorMessage) {
|
|
478
|
+
this.statusMessage = `${errorType}: ${errorMessage}`;
|
|
478
479
|
this.rootSpan.setAttributes({
|
|
479
480
|
"error.type": errorType,
|
|
480
481
|
"error.message": errorMessage,
|
|
@@ -504,7 +505,7 @@ class ProxyTracer {
|
|
|
504
505
|
"proxy.model_substituted": true,
|
|
505
506
|
"proxy.original_model": requestedModel,
|
|
506
507
|
"proxy.actual_model": actualModel,
|
|
507
|
-
"
|
|
508
|
+
"proxy.response.model": actualModel,
|
|
508
509
|
...(actualProvider ? { "proxy.actual_provider": actualProvider } : {}),
|
|
509
510
|
});
|
|
510
511
|
if (this.recordRequestMetrics) {
|
|
@@ -539,6 +540,7 @@ class ProxyTracer {
|
|
|
539
540
|
});
|
|
540
541
|
return;
|
|
541
542
|
}
|
|
543
|
+
this.input = parseBody(body);
|
|
542
544
|
this.rootSpan.addEvent("proxy.client.request_body", {
|
|
543
545
|
...buildBodyEventAttributes(body),
|
|
544
546
|
});
|
|
@@ -583,6 +585,7 @@ class ProxyTracer {
|
|
|
583
585
|
});
|
|
584
586
|
return;
|
|
585
587
|
}
|
|
588
|
+
this.output = parseBody(body);
|
|
586
589
|
this.rootSpan.addEvent("proxy.upstream.response_body", {
|
|
587
590
|
...buildBodyEventAttributes(body),
|
|
588
591
|
});
|
|
@@ -624,6 +627,7 @@ class ProxyTracer {
|
|
|
624
627
|
// -------------------------------------------------------------------------
|
|
625
628
|
/** Record an upstream retry attempt. */
|
|
626
629
|
recordRetry(account, reason) {
|
|
630
|
+
this.statusMessage ??= `retried: ${reason}`;
|
|
627
631
|
const m = getProxyMetrics();
|
|
628
632
|
m.retriesTotal.add(1, {
|
|
629
633
|
model: this.model,
|
|
@@ -687,6 +691,7 @@ class ProxyTracer {
|
|
|
687
691
|
else {
|
|
688
692
|
this.rootSpan.setStatus({ code: SpanStatusCode.OK });
|
|
689
693
|
}
|
|
694
|
+
this.observeOutcome(responseStatus);
|
|
690
695
|
this.rootSpan.end();
|
|
691
696
|
if (!this.recordRequestMetrics && !this.recordUsageMetrics) {
|
|
692
697
|
return;
|
|
@@ -759,19 +764,60 @@ class ProxyTracer {
|
|
|
759
764
|
});
|
|
760
765
|
}
|
|
761
766
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
767
|
+
levelFor(responseStatus) {
|
|
768
|
+
if (responseStatus === 499) {
|
|
769
|
+
return "WARNING";
|
|
770
|
+
}
|
|
771
|
+
if (responseStatus >= 400) {
|
|
772
|
+
return "ERROR";
|
|
773
|
+
}
|
|
774
|
+
if (this.statusMessage) {
|
|
775
|
+
return "WARNING";
|
|
776
|
+
}
|
|
777
|
+
return observationLevel(this.finishReason);
|
|
778
|
+
}
|
|
779
|
+
observeOutcome(responseStatus) {
|
|
780
|
+
const level = this.levelFor(responseStatus);
|
|
781
|
+
const statusMessage = this.statusMessage ??
|
|
782
|
+
(responseStatus >= 400 ? `HTTP ${responseStatus}` : undefined);
|
|
783
|
+
const io = {
|
|
784
|
+
input: this.input === undefined ? undefined : spanContentAttribute(this.input),
|
|
785
|
+
output: this.output === undefined
|
|
786
|
+
? undefined
|
|
787
|
+
: spanContentAttribute(this.output),
|
|
788
|
+
};
|
|
789
|
+
observe(this.rootSpan, "span", {
|
|
790
|
+
...io,
|
|
791
|
+
level,
|
|
792
|
+
statusMessage,
|
|
793
|
+
metadata: { account: this.accountEmail },
|
|
794
|
+
});
|
|
795
|
+
observeTrace(this.rootSpan, io);
|
|
796
|
+
if (this.upstreamStartedAt === undefined) {
|
|
765
797
|
return;
|
|
766
798
|
}
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
:
|
|
774
|
-
|
|
799
|
+
const generation = this.proxyTracer.startSpan(generationName(this.isStream ? "stream" : "generate"), {
|
|
800
|
+
startTime: this.upstreamStartedAt,
|
|
801
|
+
attributes: { "proxy.request_id": this.requestId },
|
|
802
|
+
}, this.traceContext);
|
|
803
|
+
observe(generation, "generation", {
|
|
804
|
+
...io,
|
|
805
|
+
model: this.responseModel ?? this.model,
|
|
806
|
+
usageDetails: this.usage && usageDetails(proxyTokenUsage(this.usage)),
|
|
807
|
+
costDetails: this.cost === undefined ? undefined : { total: this.cost },
|
|
808
|
+
completionStartTime: this.firstOutputAt === undefined
|
|
809
|
+
? undefined
|
|
810
|
+
: new Date(this.firstOutputAt),
|
|
811
|
+
level,
|
|
812
|
+
statusMessage,
|
|
813
|
+
metadata: {
|
|
814
|
+
provider: this.billingProvider,
|
|
815
|
+
account: this.accountEmail,
|
|
816
|
+
requestedModel: this.requestedModel,
|
|
817
|
+
finishReason: this.finishReason,
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
generation.end();
|
|
775
821
|
}
|
|
776
822
|
// -------------------------------------------------------------------------
|
|
777
823
|
// Context propagation
|
|
@@ -781,7 +827,9 @@ class ProxyTracer {
|
|
|
781
827
|
* Injects the current trace's `traceparent` / `tracestate` into a new header map.
|
|
782
828
|
*/
|
|
783
829
|
getTraceHeaders() {
|
|
784
|
-
|
|
830
|
+
const headers = {};
|
|
831
|
+
propagation.inject(this.traceContext, headers);
|
|
832
|
+
return headers;
|
|
785
833
|
}
|
|
786
834
|
}
|
|
787
835
|
/**
|
|
@@ -947,8 +995,6 @@ export { ProxyTracer };
|
|
|
947
995
|
/** Standard SERVER span covers every proxy door, including parsing and admission failures. */
|
|
948
996
|
export function startProxyHttpTrace(metadata, headers) {
|
|
949
997
|
try {
|
|
950
|
-
// Match OtelBridge's existing compatibility policy for HTTP-combined
|
|
951
|
-
// traceparent values: retain the first injected parent.
|
|
952
998
|
const normalizedHeaders = { ...headers };
|
|
953
999
|
if (normalizedHeaders.traceparent?.includes(",")) {
|
|
954
1000
|
normalizedHeaders.traceparent = normalizedHeaders.traceparent
|
|
@@ -956,7 +1002,7 @@ export function startProxyHttpTrace(metadata, headers) {
|
|
|
956
1002
|
.trim();
|
|
957
1003
|
}
|
|
958
1004
|
const parent = propagation.extract(ROOT_CONTEXT, normalizedHeaders);
|
|
959
|
-
const span = getTracer("
|
|
1005
|
+
const span = trace.getTracer("proxy").startSpan("proxy.http.request", {
|
|
960
1006
|
kind: SpanKind.SERVER,
|
|
961
1007
|
attributes: {
|
|
962
1008
|
"http.request.method": metadata.method,
|
|
@@ -336,7 +336,6 @@ function createTranslationRequest(args) {
|
|
|
336
336
|
inputIncludesCachedTokens: e.inputIncludesCachedTokens,
|
|
337
337
|
reasoningTokens: e.reasoningTokens,
|
|
338
338
|
});
|
|
339
|
-
tracer?.recordMetrics();
|
|
340
339
|
}
|
|
341
340
|
const ownsClientOutcome = getProxyRequestAccounting(ctx.requestId)?.accountingScope !== "internal";
|
|
342
341
|
if (errorType) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Proxy Request Logger
|
|
3
3
|
* Logs proxy request/response metadata to a rotating log file.
|
|
4
4
|
* Also emits OTLP log records to OpenObserve (or any OTLP-compatible backend)
|
|
5
|
-
* when
|
|
5
|
+
* when OTEL_EXPORTER_OTLP_ENDPOINT configures an OTLP LoggerProvider.
|
|
6
6
|
* Useful for debugging and auditing proxy traffic.
|
|
7
7
|
*/
|
|
8
8
|
import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry, ProxyRequestLoggerSnapshot } from "../types/index.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Proxy Request Logger
|
|
3
3
|
* Logs proxy request/response metadata to a rotating log file.
|
|
4
4
|
* Also emits OTLP log records to OpenObserve (or any OTLP-compatible backend)
|
|
5
|
-
* when
|
|
5
|
+
* when OTEL_EXPORTER_OTLP_ENDPOINT configures an OTLP LoggerProvider.
|
|
6
6
|
* Useful for debugging and auditing proxy traffic.
|
|
7
7
|
*/
|
|
8
8
|
import { emitProxyOtelEvent, getProxyOtelLogSnapshot, initializeProxyOtelLogs, isProxyOtelOnly, publishProxyOtelBody, } from "./otelLogSink.js";
|
|
@@ -18,9 +18,12 @@ import { writeFile } from "fs/promises";
|
|
|
18
18
|
import { setImmediate as yieldToRequests } from "node:timers/promises";
|
|
19
19
|
import { captureProxyBody, getBodyCaptureWorkerSnapshot, PROXY_BODY_CAPTURE_DEADLINE_MS, } from "./bodyCaptureWorker.js";
|
|
20
20
|
import { prepareProxyBodyForLogging as prepareRedactedBody, redactProxyHeadersForLogging as redactHeaders, splitUtf8StringByBytes, } from "./bodyCaptureProcessing.js";
|
|
21
|
+
import { trace } from "@opentelemetry/api";
|
|
21
22
|
import { isBorrowedRequest } from "./shareContext.js";
|
|
22
|
-
import { OtelBridge } from "../observability/otelBridge.js";
|
|
23
23
|
import { createRequire } from "node:module";
|
|
24
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
25
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
26
|
+
import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
|
|
24
27
|
/**
|
|
25
28
|
* Loaded via require() on first actual use rather than a top-level import,
|
|
26
29
|
* so requiring this module (reachable eagerly from dist/server/index.js)
|
|
@@ -155,6 +158,7 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_FLUSH_TIMEOUT_MS)
|
|
|
155
158
|
throw new Error(`Timed out flushing ${remaining} proxy request log operation(s)`);
|
|
156
159
|
}
|
|
157
160
|
}
|
|
161
|
+
await otlpLoggerProvider?.forceFlush();
|
|
158
162
|
}
|
|
159
163
|
/** @internal Test-only hook for exercising shutdown behavior without real I/O. */
|
|
160
164
|
export const __requestLoggerTestHooks = {
|
|
@@ -175,15 +179,8 @@ export const __requestLoggerTestHooks = {
|
|
|
175
179
|
appendMetadataFile = writeFile;
|
|
176
180
|
},
|
|
177
181
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
* null = not resolved yet (will retry), LoggerProvider = resolved, false = permanently unavailable.
|
|
181
|
-
*/
|
|
182
|
-
let otelLoggerProvider = null;
|
|
183
|
-
/** Number of times we've tried to resolve the LoggerProvider. */
|
|
184
|
-
let otelResolveAttempts = 0;
|
|
185
|
-
/** Max number of resolve attempts before giving up. */
|
|
186
|
-
const MAX_RESOLVE_ATTEMPTS = 10;
|
|
182
|
+
let otlpLoggerProvider;
|
|
183
|
+
let otlpLoggerInitialized = false;
|
|
187
184
|
const BODY_OTLP_CHUNK_SIZE = 16_000;
|
|
188
185
|
/**
|
|
189
186
|
* Initialize private request logs and preserve required lifecycle admission on startup failures.
|
|
@@ -365,45 +362,43 @@ export async function logRequestAttempt(entry) {
|
|
|
365
362
|
// Non-fatal — don't crash proxy for logging failures
|
|
366
363
|
}
|
|
367
364
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
* hasn't been loaded or OTLP is not configured, this is a no-op.
|
|
372
|
-
* Retries up to MAX_RESOLVE_ATTEMPTS times to handle race conditions
|
|
373
|
-
* where OTel initialization completes after the first log request.
|
|
374
|
-
*/
|
|
375
|
-
async function resolveLoggerProvider() {
|
|
376
|
-
if (isProxyOtelOnly()) {
|
|
377
|
-
return initializeProxyOtelLogs();
|
|
378
|
-
}
|
|
379
|
-
if (otelLoggerProvider === false) {
|
|
365
|
+
function createOtlpLoggerProvider() {
|
|
366
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
367
|
+
if (!endpoint) {
|
|
380
368
|
return undefined;
|
|
381
|
-
} // permanently unavailable
|
|
382
|
-
if (otelLoggerProvider !== null) {
|
|
383
|
-
return otelLoggerProvider;
|
|
384
369
|
}
|
|
385
|
-
// Not resolved yet — try to resolve
|
|
386
|
-
otelResolveAttempts++;
|
|
387
370
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
371
|
+
return new LoggerProvider({
|
|
372
|
+
resource: resourceFromAttributes({
|
|
373
|
+
"service.name": process.env.OTEL_SERVICE_NAME || "neurolink",
|
|
374
|
+
}),
|
|
375
|
+
processors: [
|
|
376
|
+
new BatchLogRecordProcessor(new OTLPLogExporter({ url: `${endpoint}/v1/logs` }), {
|
|
377
|
+
maxQueueSize: 2048,
|
|
378
|
+
maxExportBatchSize: 64,
|
|
379
|
+
scheduledDelayMillis: 2000,
|
|
380
|
+
exportTimeoutMillis: 30000,
|
|
381
|
+
}),
|
|
382
|
+
],
|
|
383
|
+
});
|
|
400
384
|
}
|
|
401
|
-
catch {
|
|
402
|
-
|
|
403
|
-
|
|
385
|
+
catch (error) {
|
|
386
|
+
logger.warn("[RequestLogger] Failed to create OTLP log exporter", {
|
|
387
|
+
error: error instanceof Error ? error.message : String(error),
|
|
388
|
+
});
|
|
404
389
|
return undefined;
|
|
405
390
|
}
|
|
406
391
|
}
|
|
392
|
+
async function resolveLoggerProvider() {
|
|
393
|
+
if (isProxyOtelOnly()) {
|
|
394
|
+
return initializeProxyOtelLogs();
|
|
395
|
+
}
|
|
396
|
+
if (!otlpLoggerInitialized) {
|
|
397
|
+
otlpLoggerInitialized = true;
|
|
398
|
+
otlpLoggerProvider = createOtlpLoggerProvider();
|
|
399
|
+
}
|
|
400
|
+
return otlpLoggerProvider;
|
|
401
|
+
}
|
|
407
402
|
/**
|
|
408
403
|
* Emit a RequestLogEntry as an OTLP log record.
|
|
409
404
|
* Non-blocking, non-fatal — failures are silently swallowed.
|
|
@@ -837,8 +832,7 @@ export async function logStreamError(entry) {
|
|
|
837
832
|
if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
|
|
838
833
|
return;
|
|
839
834
|
}
|
|
840
|
-
const
|
|
841
|
-
const traceCtx = bridge.getCurrentTraceContext();
|
|
835
|
+
const traceCtx = trace.getActiveSpan()?.spanContext();
|
|
842
836
|
const logEntry = {
|
|
843
837
|
...entry,
|
|
844
838
|
responseStatus: 200,
|
|
@@ -20,7 +20,7 @@ export function createTimingMiddleware() {
|
|
|
20
20
|
name: "timing",
|
|
21
21
|
order: 0, // Run first
|
|
22
22
|
handler: async (ctx, next) => {
|
|
23
|
-
return tracers.middleware.startActiveSpan("
|
|
23
|
+
return tracers.middleware.startActiveSpan("middleware.timing", async (otelSpan) => {
|
|
24
24
|
try {
|
|
25
25
|
const startTime = Date.now();
|
|
26
26
|
const startHrTime = process.hrtime.bigint();
|
|
@@ -74,7 +74,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
74
74
|
}
|
|
75
75
|
const request = validation.data;
|
|
76
76
|
return withSpan({
|
|
77
|
-
name: "
|
|
77
|
+
name: "http.execute",
|
|
78
78
|
tracer: tracers.http,
|
|
79
79
|
attributes: {
|
|
80
80
|
"http.route": "/api/agent/execute",
|
|
@@ -165,7 +165,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
165
165
|
// Wrap stream with a span that stays open for the full consumption
|
|
166
166
|
// lifetime, not just the generator creation.
|
|
167
167
|
async function* redactedStream() {
|
|
168
|
-
const streamSpan = tracers.http.startSpan("
|
|
168
|
+
const streamSpan = tracers.http.startSpan("http.stream", {
|
|
169
169
|
attributes: {
|
|
170
170
|
"http.route": "/api/agent/stream",
|
|
171
171
|
"ai.provider": request.provider || "default",
|
|
@@ -221,7 +221,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
221
221
|
try {
|
|
222
222
|
const providerName = request.provider || "openai";
|
|
223
223
|
return await withSpan({
|
|
224
|
-
name: "
|
|
224
|
+
name: "http.embed",
|
|
225
225
|
tracer: tracers.http,
|
|
226
226
|
attributes: {
|
|
227
227
|
"http.route": "/api/agent/embed",
|
|
@@ -260,7 +260,7 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
260
260
|
try {
|
|
261
261
|
const providerName = request.provider || "openai";
|
|
262
262
|
return await withSpan({
|
|
263
|
-
name: "
|
|
263
|
+
name: "http.embedMany",
|
|
264
264
|
tracer: tracers.http,
|
|
265
265
|
attributes: {
|
|
266
266
|
"http.route": "/api/agent/embed-many",
|