@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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +412 -412
- package/dist/cli/commands/proxy.js +76 -13
- package/dist/proxy/bodyCaptureWorker.js +26 -1
- package/dist/proxy/codexUsage.js +83 -2
- package/dist/proxy/otelLogSink.d.ts +13 -0
- package/dist/proxy/otelLogSink.js +62 -1
- package/dist/proxy/proxyTraceContext.d.ts +21 -0
- package/dist/proxy/proxyTraceContext.js +47 -0
- package/dist/proxy/proxyTracer.d.ts +9 -5
- package/dist/proxy/proxyTracer.js +81 -2
- package/dist/proxy/requestLogger.js +24 -18
- package/dist/server/routes/codexProxyRoutes.js +414 -332
- package/dist/types/cli.d.ts +7 -1
- package/dist/types/proxy.d.ts +119 -3
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/check-proxy-telemetry.mjs +29 -241
- package/scripts/observability/proxy-telemetry-backend.mjs +202 -0
- package/scripts/observability/proxy-telemetry-check.mjs +544 -0
- package/scripts/observability/query-proxy-history.mjs +30 -19
|
@@ -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
|
|
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
|
|
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));
|