@juspay/neurolink 12.14.4 → 12.14.5

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.
@@ -5,7 +5,8 @@
5
5
  * when a LoggerProvider is configured via OpenTelemetry instrumentation.
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
- import { emitProxyOtelEvent, getProxyOtelLogSnapshot, initializeProxyOtelLogs, isProxyOtelOnly, } from "./otelLogSink.js";
8
+ import { emitProxyOtelEvent, getProxyOtelLogSnapshot, initializeProxyOtelLogs, isProxyOtelOnly, publishProxyOtelBody, } from "./otelLogSink.js";
9
+ import { randomUUID } from "node:crypto";
9
10
  import { join } from "path";
10
11
  import { homedir } from "os";
11
12
  import { logger } from "../utils/logger.js";
@@ -18,7 +19,7 @@ import { isBorrowedRequest } from "./shareContext.js";
18
19
  import { OtelBridge } from "../observability/otelBridge.js";
19
20
  import { SeverityNumber } from "@opentelemetry/api-logs";
20
21
  import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
21
- import { notifyProxyFinalLog } from "./proxyActivity.js";
22
+ import { notifyProxyFinalLog, notifyProxyAttemptLog } from "./proxyActivity.js";
22
23
  import { withTimeout } from "../utils/async/withTimeout.js";
23
24
  let logDir = null;
24
25
  let logEnabled = false;
@@ -227,9 +228,6 @@ export async function logRequest(entry) {
227
228
  * or OTLP-derived dashboard panels.
228
229
  */
229
230
  export async function logRequestAttempt(entry) {
230
- if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
231
- return;
232
- }
233
231
  if (!entry.traceId) {
234
232
  const bridge = new OtelBridge();
235
233
  const traceCtx = bridge.getCurrentTraceContext();
@@ -238,6 +236,10 @@ export async function logRequestAttempt(entry) {
238
236
  entry.spanId = traceCtx.spanId;
239
237
  }
240
238
  }
239
+ notifyProxyAttemptLog(entry);
240
+ if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
241
+ return;
242
+ }
241
243
  if (isProxyOtelOnly()) {
242
244
  emitProxyOtelEvent("attempt", entry);
243
245
  return;
@@ -445,16 +447,11 @@ function emitOtlpBodyLogRecord(entry, stored) {
445
447
  return resolveLoggerProvider()
446
448
  .then(async (provider) => {
447
449
  if (!provider || stored.redactedBody === undefined) {
448
- return;
450
+ return undefined;
449
451
  }
450
452
  const otelLogger = provider.getLogger("neurolink-proxy-bodies", "1.0.0");
451
- const chunks = splitUtf8StringByBytes(stored.redactedBody, BODY_OTLP_CHUNK_SIZE);
452
- const totalChunks = Math.max(1, chunks.length);
453
- for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
454
- if (chunkIndex > 0 && chunkIndex % 4 === 0) {
455
- await yieldToRequests();
456
- }
457
- const chunk = chunks[chunkIndex] ?? "";
453
+ const captureId = entry.captureId ?? randomUUID();
454
+ const emit = (chunk, chunkIndex, totalChunks) => {
458
455
  otelLogger.emit({
459
456
  severityNumber: (entry.responseStatus ?? 0) >= 400
460
457
  ? SeverityNumber.WARN
@@ -466,6 +463,7 @@ function emitOtlpBodyLogRecord(entry, stored) {
466
463
  "proxy.record_kind": "body",
467
464
  "request.id": entry.requestId,
468
465
  "body.phase": entry.phase,
466
+ "body.capture_id": captureId,
469
467
  "body.chunk_index": chunkIndex,
470
468
  "body.chunk_count": totalChunks,
471
469
  "body.content_type": entry.contentType ?? "application/json",
@@ -500,10 +498,22 @@ function emitOtlpBodyLogRecord(entry, stored) {
500
498
  source: "otlp",
501
499
  },
502
500
  });
501
+ };
502
+ if (isProxyOtelOnly()) {
503
+ return publishProxyOtelBody(captureId, stored.redactedBody, emit);
503
504
  }
505
+ const chunks = splitUtf8StringByBytes(stored.redactedBody, BODY_OTLP_CHUNK_SIZE);
506
+ for (let i = 0; i < chunks.length; i++) {
507
+ if (i > 0 && i % 4 === 0) {
508
+ await yieldToRequests();
509
+ }
510
+ emit(chunks[i], i, chunks.length);
511
+ }
512
+ return undefined;
504
513
  })
505
514
  .catch(() => {
506
515
  // Non-fatal — never crash proxy for OTLP log failures
516
+ return undefined;
507
517
  });
508
518
  }
509
519
  /** Capture an owned request body with bounded processing and tracked index/export publication. */
@@ -525,7 +535,11 @@ export async function logBodyCapture(entry) {
525
535
  const destination = logDir;
526
536
  // Publication callbacks retain metadata and the bounded redacted result,
527
537
  // never the original unbounded body while a sink is slow.
528
- const metadata = { ...entry, body: undefined };
538
+ const metadata = {
539
+ ...entry,
540
+ captureId: entry.captureId ?? randomUUID(),
541
+ body: undefined,
542
+ };
529
543
  /** Persist the processed capture index and publish its redacted body before releasing capacity. */
530
544
  const consume = async (processed) => {
531
545
  const redactedHeaders = processed.headers;
@@ -538,6 +552,7 @@ export async function logBodyCapture(entry) {
538
552
  timestamp: metadata.timestamp,
539
553
  type: "body_capture",
540
554
  requestId: metadata.requestId,
555
+ captureId: metadata.captureId,
541
556
  phase: metadata.phase,
542
557
  model: metadata.model,
543
558
  stream: metadata.stream,
@@ -554,6 +569,8 @@ export async function logBodyCapture(entry) {
554
569
  redactedBodyBytes: stored.redactedBodyBytes,
555
570
  storedFileBytes: stored.storedFileBytes,
556
571
  bodyTruncated: stored.bodyTruncated,
572
+ bodyCaptureLimitBytes: stored.bodyCaptureLimitBytes,
573
+ originalRedactedBodyBytes: stored.originalRedactedBodyBytes,
557
574
  bodyWriteFailed: stored.bodyWriteFailed,
558
575
  captureError: processed.error,
559
576
  captureQueueWaitMs: processed.queueWaitMs,
@@ -565,7 +582,21 @@ export async function logBodyCapture(entry) {
565
582
  indexEntry.spanId = traceCtx.spanId;
566
583
  }
567
584
  if (isProxyOtelOnly()) {
585
+ const delivery = await emitOtlpBodyLogRecord({
586
+ ...metadata,
587
+ traceId: traceCtx?.traceId ?? metadata.traceId,
588
+ spanId: traceCtx?.spanId ?? metadata.spanId,
589
+ }, stored);
590
+ indexEntry.bodyDelivery = delivery ?? {
591
+ status: processed.error
592
+ ? "capture_rejected"
593
+ : stored.redactedBody === undefined
594
+ ? "no_body"
595
+ : "export_unconfirmed",
596
+ ...(processed.error ? { reason: processed.error } : {}),
597
+ };
568
598
  emitProxyOtelEvent("body_capture_index", indexEntry);
599
+ return;
569
600
  }
570
601
  try {
571
602
  if (logFile) {
@@ -583,7 +614,14 @@ export async function logBodyCapture(entry) {
583
614
  spanId: traceCtx?.spanId ?? metadata.spanId,
584
615
  }, stored);
585
616
  };
586
- return trackLogOperation(captureProxyBody(entry, destination, consume));
617
+ const operation = trackLogOperation(captureProxyBody(entry, destination, consume));
618
+ // HTTP handlers may await this function. Collector latency must never hold
619
+ // their response open; shutdown uses flushRequestLogs as the completion fence.
620
+ if (isProxyOtelOnly()) {
621
+ void operation;
622
+ return;
623
+ }
624
+ return operation;
587
625
  }
588
626
  /**
589
627
  * Log the FULL raw request and response for debugging.
@@ -641,8 +641,31 @@ export type ProxyBodyCaptureWorkerSnapshot = {
641
641
  pendingBytes: number;
642
642
  maxPending: number;
643
643
  maxPendingBytes: number;
644
+ /** Admission failures by exact guard, independent of processing failures. */
645
+ rejectionReasons: Record<string, number>;
644
646
  lastError?: string;
645
647
  };
648
+ /** Collector transport evidence; acknowledgement does not prove backend storage. */
649
+ export type ProxyBodyDeliveryResult = {
650
+ status: "transport_acknowledged" | "export_unconfirmed" | "rejected" | "partial";
651
+ /** Absent when publication was rejected before chunking. */
652
+ expectedChunks?: number;
653
+ acknowledgedChunks: number;
654
+ unconfirmedChunks: number;
655
+ droppedChunks: number;
656
+ notSubmittedChunks?: number;
657
+ reason?: string;
658
+ };
659
+ /** One bounded body publication, tracked across exporter callbacks. */
660
+ export type ProxyBodyPublicationProgress = {
661
+ acknowledged: number;
662
+ unconfirmed: number;
663
+ dropped: number;
664
+ emitted: number;
665
+ notify?: () => void;
666
+ };
667
+ /** Chunk emission stays in the request logger, which owns request attributes. */
668
+ export type ProxyBodyChunkEmitter = (chunk: string, index: number, count: number) => void;
646
669
  export type ProxyRequestLoggerSnapshot = {
647
670
  diskEnabled?: boolean;
648
671
  otel?: ReturnType<typeof import("../proxy/otelLogSink.js").getProxyOtelLogSnapshot>;
@@ -1991,6 +2014,8 @@ export type ProxyAnalysisRoutingRecord = {
1991
2014
  };
1992
2015
  /** Request metadata retained by the HTTP adapter for terminal error logging. */
1993
2016
  export type RuntimeRequestMetadata = {
2017
+ /** Last dispatched attempt, retained until this HTTP request terminates. */
2018
+ lastUpstreamAttempt?: RequestAttemptLogEntry;
1994
2019
  requestId: string;
1995
2020
  method: string;
1996
2021
  path: string;
@@ -2022,6 +2047,8 @@ export type RawStreamCaptureResult = {
2022
2047
  };
2023
2048
  /** Single captured body/headers entry written to disk by the proxy logger. */
2024
2049
  export type ProxyBodyCaptureEntry = {
2050
+ /** Unique capture identity shared by its index and every exported chunk. */
2051
+ captureId?: string;
2025
2052
  timestamp: string;
2026
2053
  requestId: string;
2027
2054
  phase: string;
@@ -2169,6 +2196,8 @@ export type StoredBodyArtifact = {
2169
2196
  storedFileBytes?: number;
2170
2197
  redactedBody?: string;
2171
2198
  bodyTruncated?: boolean;
2199
+ bodyCaptureLimitBytes?: number;
2200
+ originalRedactedBodyBytes?: number;
2172
2201
  bodyWriteFailed?: boolean;
2173
2202
  };
2174
2203
  /** File the proxy logger tracks for rotation and cleanup. */