@juspay/neurolink 12.14.4 → 12.14.6
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 +370 -370
- package/dist/cli/commands/proxy.d.ts +1 -1
- package/dist/cli/commands/proxy.js +58 -4
- package/dist/cli/commands/proxyRestart.d.ts +3 -0
- package/dist/cli/commands/proxyRestart.js +88 -0
- package/dist/cli/parser.js +3 -1
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +19 -4
- package/dist/proxy/bodyCaptureWorker.js +35 -13
- package/dist/proxy/otelLogSink.d.ts +22 -1
- package/dist/proxy/otelLogSink.js +247 -8
- package/dist/proxy/proxyActivity.d.ts +4 -2
- package/dist/proxy/proxyActivity.js +20 -1
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +53 -15
- package/dist/proxy/restartControl.d.ts +12 -0
- package/dist/proxy/restartControl.js +283 -0
- package/dist/proxy/rollingWorkerSupervisor.js +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxy.d.ts +29 -0
- package/dist/types/proxyRestart.d.ts +49 -0
- package/dist/types/proxyRestart.js +1 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +2 -1
- package/scripts/observability/check-proxy-telemetry.mjs +20 -7
- package/scripts/observability/query-proxy-history.mjs +193 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { CommandModule } from "yargs";
|
|
13
13
|
import type { Hono } from "hono";
|
|
14
|
-
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyHealthProbe, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxySupervisorState, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../types/index.js";
|
|
14
|
+
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyHealthProbe, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyRestartSupervisorState as ProxySupervisorState, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../types/index.js";
|
|
15
15
|
import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
|
|
16
16
|
/**
|
|
17
17
|
* Drop a supervisor `version` that is not a string.
|
|
@@ -24,6 +24,7 @@ import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure
|
|
|
24
24
|
import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
|
|
25
25
|
import { openProxyWorkerLog } from "../../proxy/workerLog.js";
|
|
26
26
|
import { startRollingProxyServer } from "../../proxy/rollingProxyServer.js";
|
|
27
|
+
import { startProxyRestartControl } from "../../proxy/restartControl.js";
|
|
27
28
|
import { isProxyAuxiliaryRequest } from "../../proxy/proxyRequestKind.js";
|
|
28
29
|
import { spawnProxySocketWorker } from "../../proxy/rollingWorkerProcess.js";
|
|
29
30
|
import { PROXY_ROLLING_SUPERVISOR_ENV, PROXY_SOCKET_WORKER_ENV, } from "../../proxy/rollingWorkerProtocol.js";
|
|
@@ -1079,6 +1080,8 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1079
1080
|
requestMetadata.set(c.req.raw, metadata);
|
|
1080
1081
|
const stopObservingFinalLog = observeProxyFinalLog(metadata.requestId, (entry) => {
|
|
1081
1082
|
metadata.terminalResult = entry;
|
|
1083
|
+
}, (entry) => {
|
|
1084
|
+
metadata.lastUpstreamAttempt = { ...entry };
|
|
1082
1085
|
});
|
|
1083
1086
|
const finishActivity = metadata.rejectForUpdate
|
|
1084
1087
|
? () => undefined
|
|
@@ -1306,7 +1309,8 @@ export async function createProxyStartApp(params) {
|
|
|
1306
1309
|
const recordRuntimeError = async (metadata, status, errorType, errorMessage, options) => {
|
|
1307
1310
|
const clientMessage = options?.clientMessage ?? errorMessage;
|
|
1308
1311
|
const clientErrorType = options?.clientErrorType ?? errorType;
|
|
1309
|
-
|
|
1312
|
+
const attempt = metadata.lastUpstreamAttempt;
|
|
1313
|
+
recordFinalError(status, attempt?.account ?? PROXY_INTERNAL_ACCOUNT_LABEL, attempt?.accountType ?? PROXY_INTERNAL_ACCOUNT_TYPE, {
|
|
1310
1314
|
requestId: metadata.requestId,
|
|
1311
1315
|
errorType,
|
|
1312
1316
|
errorCode: options?.errorCode,
|
|
@@ -1322,8 +1326,13 @@ export async function createProxyStartApp(params) {
|
|
|
1322
1326
|
model: metadata.model,
|
|
1323
1327
|
stream: metadata.stream,
|
|
1324
1328
|
toolCount: metadata.toolCount,
|
|
1325
|
-
account: "",
|
|
1326
|
-
accountType: "proxy-runtime",
|
|
1329
|
+
account: attempt?.account ?? "",
|
|
1330
|
+
accountType: attempt?.accountType ?? "proxy-runtime",
|
|
1331
|
+
accountKey: attempt?.accountKey,
|
|
1332
|
+
provider: attempt?.provider,
|
|
1333
|
+
transportScope: attempt?.transportScope,
|
|
1334
|
+
traceId: attempt?.traceId,
|
|
1335
|
+
spanId: attempt?.spanId,
|
|
1327
1336
|
responseStatus: status,
|
|
1328
1337
|
responseTimeMs: Date.now() - metadata.startedAt,
|
|
1329
1338
|
errorType,
|
|
@@ -1392,6 +1401,15 @@ export async function createProxyStartApp(params) {
|
|
|
1392
1401
|
.json()
|
|
1393
1402
|
.catch(() => ({ action: undefined }));
|
|
1394
1403
|
if (payload.action === "drain") {
|
|
1404
|
+
// A rolling worker must keep admitting until the supervisor has a ready
|
|
1405
|
+
// replacement. The legacy global drain can otherwise strand the listener
|
|
1406
|
+
// behind maintenance responses when its caller stalls or disappears.
|
|
1407
|
+
if (isProxySocketWorkerProcess()) {
|
|
1408
|
+
return c.json({
|
|
1409
|
+
error: "rolling_restart_required",
|
|
1410
|
+
message: "Use neurolink proxy restart; global update drain is disabled for rolling workers.",
|
|
1411
|
+
}, 409);
|
|
1412
|
+
}
|
|
1395
1413
|
if (!markProxyDrainingForUpdate(readiness)) {
|
|
1396
1414
|
return c.json({ error: "proxy_not_ready" }, 409);
|
|
1397
1415
|
}
|
|
@@ -2769,6 +2787,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2769
2787
|
filePrefix: "proxy-supervisor",
|
|
2770
2788
|
});
|
|
2771
2789
|
let currentUpdaterPid;
|
|
2790
|
+
let restartControl;
|
|
2772
2791
|
const rollingServer = await startRollingProxyServer({
|
|
2773
2792
|
onEvent: (event) => logProxyLifecycleEvent({
|
|
2774
2793
|
event: "supervisor_event",
|
|
@@ -2799,6 +2818,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2799
2818
|
version: PROXY_VERSION,
|
|
2800
2819
|
updaterPid: currentUpdaterPid,
|
|
2801
2820
|
rolling: snapshot,
|
|
2821
|
+
restartControl: restartControl?.identity,
|
|
2802
2822
|
});
|
|
2803
2823
|
},
|
|
2804
2824
|
log: (message) => logger.always(message),
|
|
@@ -2835,13 +2855,46 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2835
2855
|
};
|
|
2836
2856
|
process.on("SIGUSR2", activatePendingUpdate);
|
|
2837
2857
|
const readinessHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2858
|
+
try {
|
|
2859
|
+
restartControl = await startProxyRestartControl({
|
|
2860
|
+
stateDir: join(homedir(), ".neurolink"),
|
|
2861
|
+
server: rollingServer,
|
|
2862
|
+
log: (message) => logger.warn(message),
|
|
2863
|
+
isUpdatePending: () => !!rollingReplacement || !!loadUpdateState()?.pendingRestartVersion,
|
|
2864
|
+
getInstalledVersion: async () => {
|
|
2865
|
+
const { execFile } = await import("node:child_process");
|
|
2866
|
+
const { promisify } = await import("node:util");
|
|
2867
|
+
const { stdout } = await promisify(execFile)(TRAMPOLINE_PATH, ["--version"], { timeout: 5_000, maxBuffer: 65_536 });
|
|
2868
|
+
return stdout.trim();
|
|
2869
|
+
},
|
|
2870
|
+
getStatus: async () => {
|
|
2871
|
+
const probeHost = readinessHost === "::" ? "::1" : readinessHost;
|
|
2872
|
+
const response = await fetch(`http://${probeHost.includes(":") ? `[${probeHost}]` : probeHost}:${rollingServer.address.port}/status`, {
|
|
2873
|
+
headers: { connection: "close" },
|
|
2874
|
+
signal: AbortSignal.timeout(3_000),
|
|
2875
|
+
});
|
|
2876
|
+
if (!response.ok) {
|
|
2877
|
+
throw new Error("Serving status is unavailable.");
|
|
2878
|
+
}
|
|
2879
|
+
return response.json();
|
|
2880
|
+
},
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
catch (error) {
|
|
2884
|
+
// A control-plane setup failure must not stop an otherwise serving proxy.
|
|
2885
|
+
logger.warn(`[proxy-supervisor] safe restart control unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2886
|
+
}
|
|
2838
2887
|
const updatePersistedUpdaterPid = (updaterPid) => {
|
|
2839
2888
|
currentUpdaterPid = updaterPid;
|
|
2840
2889
|
const state = loadProxySupervisorState();
|
|
2841
2890
|
if (!state || state.pid !== process.pid) {
|
|
2842
2891
|
return;
|
|
2843
2892
|
}
|
|
2844
|
-
saveProxySupervisorState({
|
|
2893
|
+
saveProxySupervisorState({
|
|
2894
|
+
...state,
|
|
2895
|
+
updaterPid,
|
|
2896
|
+
restartControl: restartControl?.identity,
|
|
2897
|
+
});
|
|
2845
2898
|
};
|
|
2846
2899
|
const updaterSupervisor = startUpdaterWorkerSupervisor({
|
|
2847
2900
|
spawnWorker: () => spawnProxyUpdater(readinessHost, rollingServer.address.port, process.pid, true),
|
|
@@ -2864,6 +2917,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2864
2917
|
stopping = true;
|
|
2865
2918
|
logger.always(`[proxy-supervisor] shutting down (${signal})`);
|
|
2866
2919
|
process.off("SIGUSR2", activatePendingUpdate);
|
|
2920
|
+
await restartControl?.close();
|
|
2867
2921
|
updaterSupervisor.stop();
|
|
2868
2922
|
await rollingServer.close();
|
|
2869
2923
|
await flushProxyLifecycleEvents().catch((error) => logger.warn(String(error)));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { resolveProxyPaths } from "../../proxy/proxyPaths.js";
|
|
5
|
+
import { requestProxyRestart } from "../../proxy/restartControl.js";
|
|
6
|
+
const supervisorSchema = z.object({
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
restartControl: z.object({
|
|
9
|
+
protocol: z.literal(1),
|
|
10
|
+
socketPath: z.string(),
|
|
11
|
+
instanceId: z.string().uuid(),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
export const proxyRestartCommand = {
|
|
15
|
+
command: "restart",
|
|
16
|
+
describe: "Replace the serving worker without stopping the proxy listener",
|
|
17
|
+
builder: (yargs) => yargs
|
|
18
|
+
.option("check", {
|
|
19
|
+
type: "boolean",
|
|
20
|
+
default: false,
|
|
21
|
+
description: "Check restart readiness without changing the running proxy",
|
|
22
|
+
})
|
|
23
|
+
.option("dev", {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: false,
|
|
26
|
+
description: "Use the isolated proxy state in .neurolink-dev/",
|
|
27
|
+
})
|
|
28
|
+
.option("format", {
|
|
29
|
+
type: "string",
|
|
30
|
+
choices: ["text", "json"],
|
|
31
|
+
default: "text",
|
|
32
|
+
})
|
|
33
|
+
.example("neurolink proxy restart --check", "Verify the running supervisor supports safe worker replacement")
|
|
34
|
+
.example("neurolink proxy restart", "Start a replacement, verify it, then let existing streams finish"),
|
|
35
|
+
handler: async (argv) => {
|
|
36
|
+
try {
|
|
37
|
+
const { stateDir } = resolveProxyPaths(argv.dev);
|
|
38
|
+
const saved = supervisorSchema.safeParse(JSON.parse(await readFile(join(stateDir, "proxy-supervisor-state.json"), "utf8")));
|
|
39
|
+
if (!saved.success) {
|
|
40
|
+
throw new Error("This running supervisor does not advertise safe restart support. It must first be upgraded through a separately planned service activation.");
|
|
41
|
+
}
|
|
42
|
+
const { pid, restartControl } = saved.data;
|
|
43
|
+
if (restartControl.socketPath !==
|
|
44
|
+
join(stateDir, `restart-${pid}-${restartControl.instanceId}.sock`)) {
|
|
45
|
+
throw new Error("Restart control path does not match the recorded supervisor.");
|
|
46
|
+
}
|
|
47
|
+
const socket = await lstat(restartControl.socketPath);
|
|
48
|
+
if (!socket.isSocket() ||
|
|
49
|
+
(socket.mode & 0o077) !== 0 ||
|
|
50
|
+
(process.getuid && socket.uid !== process.getuid())) {
|
|
51
|
+
throw new Error("Restart control socket ownership or permissions are invalid.");
|
|
52
|
+
}
|
|
53
|
+
const result = await requestProxyRestart(restartControl, argv.check);
|
|
54
|
+
if (result.supervisorPid !== pid) {
|
|
55
|
+
throw new Error("Restart control identity changed; outcome is unverified.");
|
|
56
|
+
}
|
|
57
|
+
if (argv.format === "json") {
|
|
58
|
+
console.info(JSON.stringify(result, null, 2));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.info(result.message);
|
|
62
|
+
console.info(`Supervisor ${result.supervisorPid}; worker ${result.workerPid ?? "unknown"}; version ${result.version ?? "unknown"}; previous workers finishing requests: ${result.drainingWorkers}.`);
|
|
63
|
+
console.info("This operation preserves the supervisor and launchd settings. It does not reinstall the service.");
|
|
64
|
+
}
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error
|
|
71
|
+
? error.message
|
|
72
|
+
: "Restart control is unavailable.";
|
|
73
|
+
if (argv.format === "json") {
|
|
74
|
+
const result = {
|
|
75
|
+
ok: false,
|
|
76
|
+
phase: "unverified",
|
|
77
|
+
message,
|
|
78
|
+
};
|
|
79
|
+
console.info(JSON.stringify(result));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.error(message);
|
|
83
|
+
}
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=proxyRestart.js.map
|
package/dist/cli/parser.js
CHANGED
|
@@ -23,6 +23,7 @@ import { ObservabilityCommandFactory } from "./commands/observability.js";
|
|
|
23
23
|
import { TelemetryCommandFactory } from "./commands/telemetry.js";
|
|
24
24
|
import { proxyStartCommand, proxyStatusCommand, proxyTelemetryCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
|
|
25
25
|
import { proxyAnalyzeCommand } from "./commands/proxyAnalyze.js";
|
|
26
|
+
import { proxyRestartCommand } from "./commands/proxyRestart.js";
|
|
26
27
|
import { proxyShareCommand } from "./commands/proxyShare.js";
|
|
27
28
|
import { proxyPeerCommand } from "./commands/proxyPeer.js";
|
|
28
29
|
import { proxyExposeCommand } from "./commands/proxyExpose.js";
|
|
@@ -218,6 +219,7 @@ export function initializeCliParser() {
|
|
|
218
219
|
builder: (yargs) => yargs
|
|
219
220
|
.command(proxyStartCommand)
|
|
220
221
|
.command(proxyStatusCommand)
|
|
222
|
+
.command(proxyRestartCommand)
|
|
221
223
|
.command(proxyShareCommand)
|
|
222
224
|
.command(proxyPeerCommand)
|
|
223
225
|
.command(proxyExposeCommand)
|
|
@@ -228,7 +230,7 @@ export function initializeCliParser() {
|
|
|
228
230
|
.command(proxyGuardCommand)
|
|
229
231
|
.command(proxyInstallCommand)
|
|
230
232
|
.command(proxyUninstallCommand)
|
|
231
|
-
.demandCommand(1, "Please specify a proxy subcommand: start, status, share <create|provision|url|list|status|pause|resume|revoke|topup|set|link|rotate|level|note|notes|receipts|delete>, peer <add|request|sync|receipts|net|redeem|list|status|test|remove|pause|resume|set>, expose, analyze, replay <export|compare>, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
233
|
+
.demandCommand(1, "Please specify a proxy subcommand: start, status, restart, share <create|provision|url|list|status|pause|resume|revoke|topup|set|link|rotate|level|note|notes|receipts|delete>, peer <add|request|sync|receipts|net|redeem|list|status|test|remove|pause|resume|set>, expose, analyze, replay <export|compare>, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
232
234
|
handler: () => { },
|
|
233
235
|
})
|
|
234
236
|
// Evaluate Command Group - Using EvaluateCommandFactory
|
|
@@ -6,6 +6,7 @@ import { gzip as gzipCallback } from "node:zlib";
|
|
|
6
6
|
const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
|
|
7
7
|
/** Maximum redacted body bytes persisted per capture entry. */
|
|
8
8
|
const MAX_CAPTURED_BODY_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_OTEL_CAPTURED_BODY_BYTES = 8 * 1024 * 1024;
|
|
9
10
|
const BODY_TRUNCATION_MARKER = "\n...[TRUNCATED]";
|
|
10
11
|
const gzip = promisify(gzipCallback);
|
|
11
12
|
/** Headers whose values must always be redacted. */
|
|
@@ -130,12 +131,15 @@ export function splitUtf8StringByBytes(input, maxBytes) {
|
|
|
130
131
|
* Apply structural redaction before enforcing the per-artifact byte
|
|
131
132
|
* ceiling.
|
|
132
133
|
*/
|
|
133
|
-
function prepareRedactedBody(body) {
|
|
134
|
+
function prepareRedactedBody(body, maxBytes = MAX_CAPTURED_BODY_BYTES) {
|
|
134
135
|
const redacted = redactBody(body);
|
|
135
136
|
if (redacted === undefined) {
|
|
136
137
|
return { truncated: false };
|
|
137
138
|
}
|
|
138
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
...truncateUtf8String(redacted, maxBytes),
|
|
141
|
+
originalBytes: utf8ByteLength(redacted),
|
|
142
|
+
};
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
141
145
|
* Write a private gzip artifact with a unique name and return its
|
|
@@ -202,7 +206,8 @@ export function redactProxyHeadersForLogging(headers) {
|
|
|
202
206
|
*/
|
|
203
207
|
export async function processProxyBodyCapture(entry, logDir) {
|
|
204
208
|
const headers = redactHeaders(entry.headers);
|
|
205
|
-
const
|
|
209
|
+
const limit = logDir === null ? MAX_OTEL_CAPTURED_BODY_BYTES : MAX_CAPTURED_BODY_BYTES;
|
|
210
|
+
const prepared = prepareRedactedBody(entry.body, limit);
|
|
206
211
|
if (logDir === null) {
|
|
207
212
|
return {
|
|
208
213
|
headers,
|
|
@@ -210,6 +215,9 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
210
215
|
redactedBody: prepared.value,
|
|
211
216
|
redactedBodyBytes: prepared.bytes,
|
|
212
217
|
bodyTruncated: prepared.truncated,
|
|
218
|
+
bodyCaptureLimitBytes: limit,
|
|
219
|
+
originalRedactedBodyBytes: prepared.originalBytes,
|
|
220
|
+
bodySha256: prepared.value === undefined ? undefined : sha256(prepared.value),
|
|
213
221
|
},
|
|
214
222
|
};
|
|
215
223
|
}
|
|
@@ -225,5 +233,12 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
225
233
|
bodyWriteFailed: true,
|
|
226
234
|
};
|
|
227
235
|
}
|
|
228
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
headers,
|
|
238
|
+
stored: {
|
|
239
|
+
...stored,
|
|
240
|
+
bodyCaptureLimitBytes: limit,
|
|
241
|
+
originalRedactedBodyBytes: prepared.originalBytes,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
229
244
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
2
|
const MAX_PENDING = 16;
|
|
3
3
|
const MAX_PENDING_BYTES = 32 * 1024 * 1024;
|
|
4
|
-
|
|
4
|
+
// Bound retained UTF-16 strings rather than a 3x UTF-8 guess. This accommodates
|
|
5
|
+
// the observed 7.3 MB JSON requests while retaining a 32 MiB aggregate pool.
|
|
6
|
+
const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
5
7
|
export const PROXY_BODY_CAPTURE_DEADLINE_MS = 20_000;
|
|
6
8
|
let worker;
|
|
7
9
|
let workerUrl;
|
|
@@ -16,6 +18,7 @@ const snapshot = {
|
|
|
16
18
|
pendingBytes: 0,
|
|
17
19
|
maxPending: MAX_PENDING,
|
|
18
20
|
maxPendingBytes: MAX_PENDING_BYTES,
|
|
21
|
+
rejectionReasons: {},
|
|
19
22
|
};
|
|
20
23
|
const pending = new Map();
|
|
21
24
|
// Bound traversal as well as the structured clone sent to the worker. Never
|
|
@@ -29,12 +32,15 @@ function estimateCloneBytes(value) {
|
|
|
29
32
|
const seen = new Set();
|
|
30
33
|
let bytes = 0, nodes = 0;
|
|
31
34
|
while (stack.length) {
|
|
32
|
-
if (++nodes > 100_000
|
|
33
|
-
|
|
35
|
+
if (++nodes > 100_000) {
|
|
36
|
+
throw new Error("body_capture_traversal_limit");
|
|
37
|
+
}
|
|
38
|
+
if (bytes > MAX_ENTRY_BYTES) {
|
|
39
|
+
throw new Error("body_capture_entry_too_large");
|
|
34
40
|
}
|
|
35
41
|
const item = stack.pop();
|
|
36
42
|
if (typeof item === "string") {
|
|
37
|
-
bytes += item.length *
|
|
43
|
+
bytes += 16 + item.length * 2;
|
|
38
44
|
continue;
|
|
39
45
|
}
|
|
40
46
|
bytes += 16;
|
|
@@ -42,23 +48,26 @@ function estimateCloneBytes(value) {
|
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
if (seen.has(item)) {
|
|
45
|
-
|
|
51
|
+
throw new Error("body_capture_unsupported_value");
|
|
46
52
|
}
|
|
47
53
|
seen.add(item);
|
|
48
54
|
if (!Array.isArray(item) &&
|
|
49
55
|
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
50
56
|
Object.getPrototypeOf(item) !== null) {
|
|
51
|
-
|
|
57
|
+
throw new Error("body_capture_unsupported_value");
|
|
52
58
|
}
|
|
53
59
|
for (const key of Object.keys(item)) {
|
|
54
|
-
bytes += key.length *
|
|
60
|
+
bytes += 16 + key.length * 2;
|
|
55
61
|
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
56
62
|
if (!descriptor || descriptor.get || descriptor.set) {
|
|
57
|
-
|
|
63
|
+
throw new Error("body_capture_unsupported_value");
|
|
58
64
|
}
|
|
59
65
|
stack.push(descriptor.value);
|
|
60
|
-
if (stack.length > 100_000
|
|
61
|
-
|
|
66
|
+
if (stack.length > 100_000) {
|
|
67
|
+
throw new Error("body_capture_traversal_limit");
|
|
68
|
+
}
|
|
69
|
+
if (bytes > MAX_ENTRY_BYTES) {
|
|
70
|
+
throw new Error("body_capture_entry_too_large");
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -125,11 +134,21 @@ function getWorker() {
|
|
|
125
134
|
export async function captureProxyBody(entry, logDir, consume) {
|
|
126
135
|
snapshot.attempted += 1;
|
|
127
136
|
let bytes;
|
|
137
|
+
let admissionError;
|
|
128
138
|
try {
|
|
129
139
|
bytes = estimateCloneBytes(entry);
|
|
130
140
|
}
|
|
131
|
-
catch {
|
|
141
|
+
catch (error) {
|
|
132
142
|
bytes = Infinity;
|
|
143
|
+
admissionError =
|
|
144
|
+
error instanceof Error &&
|
|
145
|
+
[
|
|
146
|
+
"body_capture_entry_too_large",
|
|
147
|
+
"body_capture_traversal_limit",
|
|
148
|
+
"body_capture_unsupported_value",
|
|
149
|
+
].includes(error.message)
|
|
150
|
+
? error.message
|
|
151
|
+
: "body_capture_unsupported_value";
|
|
133
152
|
}
|
|
134
153
|
if (bytes > MAX_ENTRY_BYTES ||
|
|
135
154
|
snapshot.pending >= MAX_PENDING ||
|
|
@@ -137,11 +156,13 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
137
156
|
Date.now() < retryAfter) {
|
|
138
157
|
snapshot.rejected += 1;
|
|
139
158
|
const error = bytes > MAX_ENTRY_BYTES
|
|
140
|
-
? "
|
|
159
|
+
? (admissionError ?? "body_capture_entry_too_large")
|
|
141
160
|
: Date.now() < retryAfter
|
|
142
161
|
? "body_worker_backoff"
|
|
143
162
|
: "body_capture_queue_full";
|
|
144
163
|
snapshot.lastError = error;
|
|
164
|
+
snapshot.rejectionReasons[error] =
|
|
165
|
+
(snapshot.rejectionReasons[error] ?? 0) + 1;
|
|
145
166
|
return consume({ error, stored: { bodyWriteFailed: true } });
|
|
146
167
|
}
|
|
147
168
|
let current;
|
|
@@ -204,7 +225,7 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
204
225
|
* retained publication work.
|
|
205
226
|
*/
|
|
206
227
|
export function getBodyCaptureWorkerSnapshot() {
|
|
207
|
-
return { ...snapshot };
|
|
228
|
+
return { ...snapshot, rejectionReasons: { ...snapshot.rejectionReasons } };
|
|
208
229
|
}
|
|
209
230
|
/** Isolated tests point at a separately executed built worker. */
|
|
210
231
|
export const __bodyCaptureWorkerTestHooks = {
|
|
@@ -227,6 +248,7 @@ export const __bodyCaptureWorkerTestHooks = {
|
|
|
227
248
|
pending: 0,
|
|
228
249
|
pendingBytes: 0,
|
|
229
250
|
lastError: undefined,
|
|
251
|
+
rejectionReasons: {},
|
|
230
252
|
});
|
|
231
253
|
},
|
|
232
254
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import type { ProxyBodyChunkEmitter, ProxyBodyDeliveryResult } from "../types/index.js";
|
|
2
3
|
/** Explicit opt-in; configuration never silently falls back to file logging. */
|
|
3
4
|
export declare function isProxyOtelOnly(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Own a whole capture within byte/count bounds, then pace its chunks by actual
|
|
7
|
+
* export callbacks. SDK forceFlush alone does not await an automatic export
|
|
8
|
+
* already in flight. Serial publication prevents bursts from dropping tails.
|
|
9
|
+
*/
|
|
10
|
+
export declare function publishProxyOtelBody(captureId: string, body: string, emit: ProxyBodyChunkEmitter): Promise<ProxyBodyDeliveryResult>;
|
|
4
11
|
/** Initialize a log-only provider in every proxy process, including the supervisor. */
|
|
5
12
|
export declare function initializeProxyOtelLogs(role?: string): LoggerProvider | undefined;
|
|
6
13
|
/** Structured evidence without final-request dashboard fields on auxiliary events. */
|
|
@@ -13,6 +20,19 @@ export declare function getProxyOtelLogSnapshot(): {
|
|
|
13
20
|
initialized: boolean;
|
|
14
21
|
deliveryGuarantee: string;
|
|
15
22
|
invalidRecords: number;
|
|
23
|
+
bodyDelivery: {
|
|
24
|
+
attempted: number;
|
|
25
|
+
transportAcknowledged: number;
|
|
26
|
+
exportUnconfirmed: number;
|
|
27
|
+
rejected: number;
|
|
28
|
+
partial: number;
|
|
29
|
+
pending: number;
|
|
30
|
+
pendingBytes: number;
|
|
31
|
+
highWaterPending: number;
|
|
32
|
+
highWaterBytes: number;
|
|
33
|
+
maxPending: number;
|
|
34
|
+
maxPendingBytes: number;
|
|
35
|
+
};
|
|
16
36
|
queues: {
|
|
17
37
|
attempted: number;
|
|
18
38
|
submitted: number;
|
|
@@ -22,7 +42,8 @@ export declare function getProxyOtelLogSnapshot(): {
|
|
|
22
42
|
outstanding: number;
|
|
23
43
|
lastAcknowledgedAt: string | undefined;
|
|
24
44
|
lastFailureAt: string | undefined;
|
|
25
|
-
|
|
45
|
+
highWaterOutstanding: number;
|
|
46
|
+
kind: "metadata" | "bodies";
|
|
26
47
|
capacity: number;
|
|
27
48
|
}[];
|
|
28
49
|
};
|