@juspay/neurolink 12.14.6 → 12.14.7

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.
@@ -13,12 +13,13 @@
13
13
  * - calculateCost() from pricing.ts for cost tracking
14
14
  * - TelemetryService for metrics recording
15
15
  */
16
- import { SpanStatusCode, context, metrics, trace, } from "@opentelemetry/api";
16
+ import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
17
17
  import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
18
18
  import { OtelBridge } from "../observability/otelBridge.js";
19
19
  import { calculateCost } from "../utils/pricing.js";
20
20
  import { TelemetryService } from "../telemetry/telemetryService.js";
21
21
  import { logger } from "../utils/logger.js";
22
+ import { registerProxyRequestTraceContext, releaseProxyRequestTraceContext, } from "./proxyTraceContext.js";
22
23
  const LOG_PREFIX = "[ProxyTracer]";
23
24
  // ---------------------------------------------------------------------------
24
25
  // OTEL Metric Instruments — lazy singleton
@@ -205,6 +206,8 @@ class ProxyTracer {
205
206
  billingProvider;
206
207
  startTime;
207
208
  isStream;
209
+ ended = false;
210
+ recordRequestMetrics = true;
208
211
  accountEmail;
209
212
  usage;
210
213
  mode = "full";
@@ -226,7 +229,7 @@ class ProxyTracer {
226
229
  const tracer = getTracer("neurolink.proxy");
227
230
  // Extract parent context from incoming headers (Claude Code may send traceparent)
228
231
  let parentContext = context.active();
229
- if (incomingHeaders) {
232
+ if (incomingHeaders && !trace.getSpan(context.active())) {
230
233
  const bridge = new OtelBridge();
231
234
  const extracted = bridge.extractContext(incomingHeaders);
232
235
  if (extracted) {
@@ -270,6 +273,7 @@ class ProxyTracer {
270
273
  rootSpan.setAttribute("neurolink.conversation_id", nlConversationId);
271
274
  }
272
275
  const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
276
+ instance.recordRequestMetrics = ctx.recordRequestMetrics !== false;
273
277
  // Set Langfuse context (fire-and-forget — non-blocking)
274
278
  // Prefer NeuroLink session/user from calling SDK over Claude Code session
275
279
  setLangfuseContext({
@@ -607,6 +611,7 @@ class ProxyTracer {
607
611
  return {
608
612
  traceId: spanCtx.traceId,
609
613
  spanId: spanCtx.spanId,
614
+ traceFlags: spanCtx.traceFlags,
610
615
  };
611
616
  }
612
617
  /** Return the captured usage (set by setUsage). */
@@ -618,6 +623,10 @@ class ProxyTracer {
618
623
  // -------------------------------------------------------------------------
619
624
  /** End the root span with final HTTP status and duration, and emit OTEL metrics. */
620
625
  end(responseStatus, durationMs) {
626
+ if (this.ended) {
627
+ return;
628
+ }
629
+ this.ended = true;
621
630
  this.rootSpan.setAttributes({
622
631
  "http.status_code": responseStatus,
623
632
  "proxy.duration_ms": durationMs,
@@ -636,6 +645,9 @@ class ProxyTracer {
636
645
  this.rootSpan.setStatus({ code: SpanStatusCode.OK });
637
646
  }
638
647
  this.rootSpan.end();
648
+ if (!this.recordRequestMetrics) {
649
+ return;
650
+ }
639
651
  // ---- Emit OTEL metrics (lazy-init instruments) ----
640
652
  const m = getProxyMetrics();
641
653
  const labels = {
@@ -745,3 +757,70 @@ export function recordFallbackAttempt(attrs) {
745
757
  }
746
758
  }
747
759
  export { ProxyTracer };
760
+ /** Standard SERVER span covers every proxy door, including parsing and admission failures. */
761
+ export function startProxyHttpTrace(metadata, headers) {
762
+ try {
763
+ // Match OtelBridge's existing compatibility policy for HTTP-combined
764
+ // traceparent values: retain the first injected parent.
765
+ const normalizedHeaders = { ...headers };
766
+ if (normalizedHeaders.traceparent?.includes(",")) {
767
+ normalizedHeaders.traceparent = normalizedHeaders.traceparent
768
+ .split(",", 1)[0]
769
+ .trim();
770
+ }
771
+ const parent = propagation.extract(ROOT_CONTEXT, normalizedHeaders);
772
+ const span = getTracer("neurolink.proxy").startSpan("proxy.http.request", {
773
+ kind: SpanKind.SERVER,
774
+ attributes: {
775
+ "http.request.method": metadata.method,
776
+ "url.path": metadata.path,
777
+ "proxy.request_id": metadata.requestId,
778
+ },
779
+ }, parent);
780
+ const spanContext = span.spanContext();
781
+ if (isSpanContextValid(spanContext)) {
782
+ metadata.traceId = spanContext.traceId;
783
+ metadata.spanId = spanContext.spanId;
784
+ metadata.traceFlags = spanContext.traceFlags;
785
+ registerProxyRequestTraceContext(metadata.requestId, {
786
+ traceId: spanContext.traceId,
787
+ spanId: spanContext.spanId,
788
+ traceFlags: spanContext.traceFlags,
789
+ });
790
+ }
791
+ const active = trace.setSpan(parent, span);
792
+ let ended = false;
793
+ return {
794
+ run: (fn) => context.with(active, fn),
795
+ end: (status, outcome, errorType) => {
796
+ if (ended) {
797
+ return;
798
+ }
799
+ ended = true;
800
+ try {
801
+ span.setAttributes({
802
+ "http.response.status_code": status,
803
+ "proxy.terminal_outcome": outcome,
804
+ ...(errorType ? { "error.type": errorType } : {}),
805
+ });
806
+ if (status >= 500 ||
807
+ outcome === "stream_error" ||
808
+ outcome === "unknown") {
809
+ span.setStatus({ code: SpanStatusCode.ERROR });
810
+ }
811
+ span.end();
812
+ }
813
+ catch {
814
+ // Telemetry must not interrupt transport cleanup.
815
+ }
816
+ finally {
817
+ releaseProxyRequestTraceContext(metadata.requestId);
818
+ }
819
+ },
820
+ };
821
+ }
822
+ catch {
823
+ releaseProxyRequestTraceContext(metadata.requestId);
824
+ return { run: (fn) => fn(), end: () => undefined };
825
+ }
826
+ }
@@ -21,6 +21,7 @@ import { SeverityNumber } from "@opentelemetry/api-logs";
21
21
  import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
22
22
  import { notifyProxyFinalLog, notifyProxyAttemptLog } from "./proxyActivity.js";
23
23
  import { withTimeout } from "../utils/async/withTimeout.js";
24
+ import { resolveProxyLogTraceContext, proxyLogContext, } from "./proxyTraceContext.js";
24
25
  let logDir = null;
25
26
  let logEnabled = false;
26
27
  const pendingLogOperations = new Set();
@@ -184,6 +185,12 @@ export function initRequestLogger(enabled = true, customLogsDir) {
184
185
  }
185
186
  }
186
187
  export async function logRequest(entry) {
188
+ if (!entry.traceId || entry.traceFlags === undefined) {
189
+ const traceCtx = resolveProxyLogTraceContext(entry);
190
+ if (traceCtx) {
191
+ Object.assign(entry, traceCtx);
192
+ }
193
+ }
187
194
  entry.terminalOutcome ??=
188
195
  entry.errorType === "client_cancelled" || entry.responseStatus === 499
189
196
  ? "client_cancelled"
@@ -196,17 +203,6 @@ export async function logRequest(entry) {
196
203
  if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
197
204
  return;
198
205
  }
199
- // Only use OtelBridge if traceId not already provided by caller.
200
- // Deferred .then() callbacks lose async context, so OtelBridge would
201
- // return undefined and overwrite the valid traceId the caller passed.
202
- if (!entry.traceId) {
203
- const bridge = new OtelBridge();
204
- const traceCtx = bridge.getCurrentTraceContext();
205
- if (traceCtx) {
206
- entry.traceId = traceCtx.traceId;
207
- entry.spanId = traceCtx.spanId;
208
- }
209
- }
210
206
  if (isProxyOtelOnly()) {
211
207
  await emitOtlpLogRecord(entry);
212
208
  return;
@@ -228,12 +224,12 @@ export async function logRequest(entry) {
228
224
  * or OTLP-derived dashboard panels.
229
225
  */
230
226
  export async function logRequestAttempt(entry) {
231
- if (!entry.traceId) {
232
- const bridge = new OtelBridge();
233
- const traceCtx = bridge.getCurrentTraceContext();
227
+ if (!entry.traceId || entry.traceFlags === undefined) {
228
+ const traceCtx = resolveProxyLogTraceContext(entry);
234
229
  if (traceCtx) {
235
230
  entry.traceId = traceCtx.traceId;
236
231
  entry.spanId = traceCtx.spanId;
232
+ entry.traceFlags = traceCtx.traceFlags;
237
233
  }
238
234
  }
239
235
  notifyProxyAttemptLog(entry);
@@ -313,6 +309,7 @@ function emitOtlpLogRecord(entry) {
313
309
  : SeverityNumber.INFO;
314
310
  const severityText = isError ? (isRateLimit ? "WARN" : "ERROR") : "INFO";
315
311
  otelLogger.emit({
312
+ context: proxyLogContext(entry),
316
313
  severityNumber,
317
314
  severityText,
318
315
  body: isProxyOtelOnly()
@@ -453,6 +450,7 @@ function emitOtlpBodyLogRecord(entry, stored) {
453
450
  const captureId = entry.captureId ?? randomUUID();
454
451
  const emit = (chunk, chunkIndex, totalChunks) => {
455
452
  otelLogger.emit({
453
+ context: proxyLogContext(entry),
456
454
  severityNumber: (entry.responseStatus ?? 0) >= 400
457
455
  ? SeverityNumber.WARN
458
456
  : SeverityNumber.INFO,
@@ -526,12 +524,16 @@ export async function logBodyCapture(entry) {
526
524
  // not something a share token can be read as consenting to. The request is
527
525
  // still logged; only the bodies are dropped.
528
526
  if (isBorrowedRequest()) {
527
+ emitProxyOtelEvent("body_capture_index", {
528
+ timestamp: entry.timestamp,
529
+ requestId: entry.requestId,
530
+ captureId: entry.captureId ?? randomUUID(),
531
+ phase: entry.phase,
532
+ bodyDelivery: { status: "policy_excluded", reason: "borrowed_traffic" },
533
+ });
529
534
  return;
530
535
  }
531
- const bridge = new OtelBridge();
532
- const traceCtx = entry.traceId && entry.spanId
533
- ? { traceId: entry.traceId, spanId: entry.spanId }
534
- : bridge.getCurrentTraceContext();
536
+ const traceCtx = resolveProxyLogTraceContext(entry);
535
537
  const destination = logDir;
536
538
  // Publication callbacks retain metadata and the bounded redacted result,
537
539
  // never the original unbounded body while a sink is slow.
@@ -573,6 +575,7 @@ export async function logBodyCapture(entry) {
573
575
  originalRedactedBodyBytes: stored.originalRedactedBodyBytes,
574
576
  bodyWriteFailed: stored.bodyWriteFailed,
575
577
  captureError: processed.error,
578
+ captureAdmission: processed.admission,
576
579
  captureQueueWaitMs: processed.queueWaitMs,
577
580
  captureProcessingMs: processed.processingMs,
578
581
  metadata: processed.error ? undefined : metadata.metadata,
@@ -580,12 +583,14 @@ export async function logBodyCapture(entry) {
580
583
  if (traceCtx) {
581
584
  indexEntry.traceId = traceCtx.traceId;
582
585
  indexEntry.spanId = traceCtx.spanId;
586
+ indexEntry.traceFlags = traceCtx.traceFlags;
583
587
  }
584
588
  if (isProxyOtelOnly()) {
585
589
  const delivery = await emitOtlpBodyLogRecord({
586
590
  ...metadata,
587
591
  traceId: traceCtx?.traceId ?? metadata.traceId,
588
592
  spanId: traceCtx?.spanId ?? metadata.spanId,
593
+ traceFlags: traceCtx?.traceFlags ?? metadata.traceFlags,
589
594
  }, stored);
590
595
  indexEntry.bodyDelivery = delivery ?? {
591
596
  status: processed.error
@@ -612,6 +617,7 @@ export async function logBodyCapture(entry) {
612
617
  ...metadata,
613
618
  traceId: traceCtx?.traceId ?? metadata.traceId,
614
619
  spanId: traceCtx?.spanId ?? metadata.spanId,
620
+ traceFlags: traceCtx?.traceFlags ?? metadata.traceFlags,
615
621
  }, stored);
616
622
  };
617
623
  const operation = trackLogOperation(captureProxyBody(entry, destination, consume));