@juspay/neurolink 12.14.0 → 12.14.1

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.
@@ -88,5 +88,6 @@ export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
88
88
  export declare const proxyTelemetryCommand: CommandModule<object, ProxyTelemetryArgs>;
89
89
  export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
90
90
  export declare const proxySetupCommand: CommandModule;
91
+ export declare function buildProxyLaunchdPlist(port: number, host: string, envFile?: string, configFile?: string): string;
91
92
  export declare const proxyInstallCommand: CommandModule;
92
93
  export declare const proxyUninstallCommand: CommandModule;
@@ -1,14 +1,5 @@
1
- /**
2
- * Proxy CLI Commands for NeuroLink
3
- *
4
- * Implements commands for managing the Claude multi-account proxy:
5
- * - neurolink proxy start — Start the proxy server
6
- * - neurolink proxy status — Show proxy status (accounts, sessions, routing)
7
- *
8
- * The proxy creates a NeuroLink instance and builds a Hono app that registers
9
- * Claude-compatible proxy routes. All requests flow through ctx.neurolink
10
- * (generate/stream), with an optional ModelRouter for model remapping.
11
- */
1
+ import { initializeProxyOtelLogs, routeProxyConsoleToOtel, flushProxyOtelLogs, shutdownProxyOtelLogs, isProxyOtelOnly, withProxyOtelLogShutdown, } from "../../proxy/otelLogSink.js";
2
+ import { writeFileAtomic } from "../proxy-clients/snapshot.js";
12
3
  import { spawn } from "node:child_process";
13
4
  import { homedir } from "node:os";
14
5
  import { dirname, join } from "node:path";
@@ -2398,6 +2389,8 @@ function registerProxyShutdownHandlers(params) {
2398
2389
  }
2399
2390
  try {
2400
2391
  const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../services/server/ai/observability/instrumentation.js");
2392
+ await flushProxyOtelLogs();
2393
+ await shutdownProxyOtelLogs();
2401
2394
  await flushOpenTelemetry();
2402
2395
  await shutdownOpenTelemetry();
2403
2396
  }
@@ -2758,6 +2751,9 @@ async function startProxyRuntime(params) {
2758
2751
  * serving-process exits.
2759
2752
  */
2760
2753
  async function runLaunchdProxySupervisor(argv, spinner) {
2754
+ await loadProxyStartEnv(argv, spinner);
2755
+ initializeProxyOtelLogs("supervisor");
2756
+ routeProxyConsoleToOtel();
2761
2757
  await ensureProxyStartAllowed(spinner);
2762
2758
  const entryScript = process.argv[1];
2763
2759
  if (!entryScript) {
@@ -2871,6 +2867,8 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2871
2867
  updaterSupervisor.stop();
2872
2868
  await rollingServer.close();
2873
2869
  await flushProxyLifecycleEvents().catch((error) => logger.warn(String(error)));
2870
+ await flushProxyOtelLogs().catch(() => undefined);
2871
+ await shutdownProxyOtelLogs().catch(() => undefined);
2874
2872
  const supervisorState = loadProxySupervisorState();
2875
2873
  if (supervisorState?.pid === process.pid) {
2876
2874
  clearProxySupervisorState();
@@ -2951,6 +2949,8 @@ async function startProxyCommandHandler(argv) {
2951
2949
  env: baseEnv,
2952
2950
  });
2953
2951
  const loadedEnvFile = await loadProxyStartEnv(argv, spinner);
2952
+ initializeProxyOtelLogs("worker");
2953
+ routeProxyConsoleToOtel();
2954
2954
  // Reuse upstream TCP connections (longer keep-alive + bounded pool) instead
2955
2955
  // of opening a new flow per request — cuts outbound flow churn through host
2956
2956
  // content-filters. Runs once, after env load so it can be tuned via env.
@@ -3592,7 +3592,9 @@ export const proxyGuardCommand = {
3592
3592
  default: true,
3593
3593
  });
3594
3594
  },
3595
- handler: async (argv) => {
3595
+ handler: withProxyOtelLogShutdown(async (argv) => {
3596
+ initializeProxyOtelLogs("updater");
3597
+ routeProxyConsoleToOtel();
3596
3598
  const host = argv.host ?? "127.0.0.1";
3597
3599
  const port = argv.port ?? 55669;
3598
3600
  const parentPid = Number(argv.parentPid);
@@ -3916,6 +3918,8 @@ export const proxyGuardCommand = {
3916
3918
  logger.always(`[updater] update successful: now running ${result.latestVersion}`);
3917
3919
  persistUpdaterState("record successful update", () => recordSuccessfulUpdate(result.latestVersion));
3918
3920
  // The replacement proxy starts a worker running the new version.
3921
+ await flushProxyOtelLogs().catch(() => undefined);
3922
+ await shutdownProxyOtelLogs().catch(() => undefined);
3919
3923
  process.exit(0);
3920
3924
  }
3921
3925
  else {
@@ -4083,7 +4087,7 @@ export const proxyGuardCommand = {
4083
4087
  if (cleared && !argv.quiet) {
4084
4088
  logger.always(`[proxy] fail-open guard removed stale ${expectedBaseUrl} from Claude settings`);
4085
4089
  }
4086
- },
4090
+ }),
4087
4091
  };
4088
4092
  // =============================================================================
4089
4093
  // PROXY SETUP COMMAND
@@ -4257,7 +4261,7 @@ function buildLaunchdPath() {
4257
4261
  }
4258
4262
  return [...segments].join(":");
4259
4263
  }
4260
- function buildPlist(port, host, envFile, configFile) {
4264
+ export function buildProxyLaunchdPlist(port, host, envFile, configFile) {
4261
4265
  // The plist invokes the trampoline script (a tiny shell wrapper at
4262
4266
  // ~/.neurolink/bin/neurolink-proxy) which re-resolves the real
4263
4267
  // `neurolink` binary via PATH on every launch. This way, launchd
@@ -4273,6 +4277,20 @@ function buildPlist(port, host, envFile, configFile) {
4273
4277
  <string>--config</string>
4274
4278
  <string>${escapeXml(configFile)}</string>`
4275
4279
  : "";
4280
+ const otelEnvironment = isProxyOtelOnly()
4281
+ ? [
4282
+ "NEUROLINK_PROXY_LOG_SINK",
4283
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
4284
+ "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
4285
+ "OTEL_EXPORTER_OTLP_HEADERS",
4286
+ "OTEL_EXPORTER_OTLP_LOGS_HEADERS",
4287
+ "OTEL_SERVICE_NAME",
4288
+ "NEUROLINK_PROXY_SESSION_SECRET",
4289
+ ]
4290
+ .filter((name) => process.env[name] !== undefined)
4291
+ .map((name) => ` <key>${name}</key>\n <string>${escapeXml(process.env[name])}</string>`)
4292
+ .join("\n")
4293
+ : "";
4276
4294
  return `<?xml version="1.0" encoding="UTF-8"?>
4277
4295
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
4278
4296
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -4311,10 +4329,10 @@ ${configArgs}
4311
4329
  <integer>45</integer>
4312
4330
 
4313
4331
  <key>StandardOutPath</key>
4314
- <string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
4332
+ <string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
4315
4333
 
4316
4334
  <key>StandardErrorPath</key>
4317
- <string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
4335
+ <string>${isProxyOtelOnly() ? "/dev/null" : join(homedir(), ".neurolink", "logs", "proxy-launchd-stderr.log")}</string>
4318
4336
 
4319
4337
  <key>EnvironmentVariables</key>
4320
4338
  <dict>
@@ -4322,6 +4340,7 @@ ${configArgs}
4322
4340
  <string>${buildLaunchdPath()}</string>
4323
4341
  <key>HOME</key>
4324
4342
  <string>${homedir()}</string>
4343
+ ${otelEnvironment}
4325
4344
  </dict>
4326
4345
  </dict>
4327
4346
  </plist>`;
@@ -4362,7 +4381,7 @@ export const proxyInstallCommand = {
4362
4381
  console.info(chalk.yellow("On Linux, use systemd. On Windows, use Task Scheduler."));
4363
4382
  process.exit(1);
4364
4383
  }
4365
- const { writeFileSync, mkdirSync, existsSync } = await import("fs");
4384
+ const { mkdirSync, existsSync, chmodSync } = await import("fs");
4366
4385
  const envResolution = resolveProxyEnvFile({
4367
4386
  explicitEnvFile: argv.envFile,
4368
4387
  });
@@ -4378,8 +4397,9 @@ export const proxyInstallCommand = {
4378
4397
  console.info(chalk.red(`Proxy env file not found: ${envFile}`));
4379
4398
  process.exit(1);
4380
4399
  }
4400
+ await loadProxyEnvFile({ explicitEnvFile: envFile });
4381
4401
  const logsDir = join(homedir(), ".neurolink", "logs");
4382
- if (!existsSync(logsDir)) {
4402
+ if (!isProxyOtelOnly() && !existsSync(logsDir)) {
4383
4403
  mkdirSync(logsDir, { recursive: true });
4384
4404
  }
4385
4405
  if (!existsSync(PLIST_DIR)) {
@@ -4403,8 +4423,9 @@ export const proxyInstallCommand = {
4403
4423
  process.exit(1);
4404
4424
  }
4405
4425
  console.info(chalk.green(`✓ Trampoline validated (resolves to neurolink v${trampolineVersion})`));
4406
- const plist = buildPlist(port, host, envFile, configFile);
4407
- writeFileSync(PLIST_PATH, plist, "utf-8");
4426
+ const plist = buildProxyLaunchdPlist(port, host, envFile, configFile);
4427
+ await writeFileAtomic(PLIST_PATH, plist, 0o600);
4428
+ chmodSync(PLIST_PATH, 0o600);
4408
4429
  console.info(chalk.green(`✓ Plist written to ${PLIST_PATH}`));
4409
4430
  if (envFile) {
4410
4431
  console.info(chalk.green(`✓ Proxy env file: ${envFile}`));
@@ -19,4 +19,4 @@ export declare function redactProxyHeadersForLogging(headers: Record<string, str
19
19
  * Process a capture in the worker and retain redacted output when artifact
20
20
  * persistence fails.
21
21
  */
22
- export declare function processProxyBodyCapture(entry: ProxyBodyCaptureEntry, logDir: string): Promise<ProcessedProxyBodyCapture>;
22
+ export declare function processProxyBodyCapture(entry: ProxyBodyCaptureEntry, logDir: string | null): Promise<ProcessedProxyBodyCapture>;
@@ -203,6 +203,16 @@ export function redactProxyHeadersForLogging(headers) {
203
203
  export async function processProxyBodyCapture(entry, logDir) {
204
204
  const headers = redactHeaders(entry.headers);
205
205
  const prepared = prepareRedactedBody(entry.body);
206
+ if (logDir === null) {
207
+ return {
208
+ headers,
209
+ stored: {
210
+ redactedBody: prepared.value,
211
+ redactedBodyBytes: prepared.bytes,
212
+ bodyTruncated: prepared.truncated,
213
+ },
214
+ };
215
+ }
206
216
  let stored;
207
217
  try {
208
218
  stored = await writeBodyArtifact(logDir, entry, headers, prepared.value, prepared.truncated);
@@ -1,7 +1,7 @@
1
1
  import type { ProcessedProxyBodyCapture, ProxyBodyCaptureEntry, ProxyBodyCaptureWorkerSnapshot } from "../types/index.js";
2
2
  export declare const PROXY_BODY_CAPTURE_DEADLINE_MS = 20000;
3
3
  /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
4
- export declare function captureProxyBody(entry: ProxyBodyCaptureEntry, logDir: string, consume: (result: ProcessedProxyBodyCapture) => Promise<void>): Promise<void>;
4
+ export declare function captureProxyBody(entry: ProxyBodyCaptureEntry, logDir: string | null, consume: (result: ProcessedProxyBodyCapture) => Promise<void>): Promise<void>;
5
5
  /**
6
6
  * Return independent counters for processing, rejection, failure, and
7
7
  * retained publication work.
@@ -1,3 +1,4 @@
1
+ import { isProxyOtelOnly } from "./otelLogSink.js";
1
2
  import { Worker } from "node:worker_threads";
2
3
  import { withTimeout } from "../utils/async/withTimeout.js";
3
4
  import { logger } from "../utils/logger.js";
@@ -9,6 +10,9 @@ const WORKER_TERMINATION_TIMEOUT_MS = 5_000;
9
10
  * block active streams. Concurrent runs are coalesced into the active scan.
10
11
  */
11
12
  export function startProxyLogCleanupScheduler(params) {
13
+ if (isProxyOtelOnly()) {
14
+ return { trigger: () => false, stop: async () => { } };
15
+ }
12
16
  const maxAgeDays = params.maxAgeDays ?? 7;
13
17
  const maxSizeMb = params.maxSizeMb ?? 500;
14
18
  let activeWorker;
@@ -0,0 +1,34 @@
1
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
2
+ /** Explicit opt-in; configuration never silently falls back to file logging. */
3
+ export declare function isProxyOtelOnly(): boolean;
4
+ /** Initialize a log-only provider in every proxy process, including the supervisor. */
5
+ export declare function initializeProxyOtelLogs(role?: string): LoggerProvider | undefined;
6
+ /** Structured evidence without final-request dashboard fields on auxiliary events. */
7
+ export declare function emitProxyOtelEvent(kind: string, record: Record<string, unknown>): void;
8
+ /** Capture application console diagnostics only inside proxy service processes. */
9
+ export declare function routeProxyConsoleToOtel(): void;
10
+ /** Counters acknowledge collector transport only, never backend persistence. */
11
+ export declare function getProxyOtelLogSnapshot(): {
12
+ mode: string;
13
+ initialized: boolean;
14
+ deliveryGuarantee: string;
15
+ invalidRecords: number;
16
+ queues: {
17
+ attempted: number;
18
+ submitted: number;
19
+ transportAcknowledged: number;
20
+ exportUnconfirmed: number;
21
+ dropped: number;
22
+ outstanding: number;
23
+ lastAcknowledgedAt: string | undefined;
24
+ lastFailureAt: string | undefined;
25
+ kind: string;
26
+ capacity: number;
27
+ }[];
28
+ };
29
+ /** Bounded provider flush belongs after final request and lifecycle publication. */
30
+ export declare function flushProxyOtelLogs(): Promise<void>;
31
+ /** Release this process's exporter and restore console ownership. */
32
+ export declare function shutdownProxyOtelLogs(): Promise<void>;
33
+ /** Flush short-lived proxy command diagnostics on every normal return or exception. */
34
+ export declare function withProxyOtelLogShutdown<TArg>(handler: (arg: TArg) => Promise<void>): (arg: TArg) => Promise<void>;
@@ -0,0 +1,254 @@
1
+ /* eslint-disable no-console -- This proxy-only sink replaces console methods with OTLP emission. */
2
+ import { inspect } from "node:util";
3
+ import { SeverityNumber } from "@opentelemetry/api-logs";
4
+ import { ExportResultCode } from "@opentelemetry/core";
5
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
+ import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
8
+ import { sanitizeForLog } from "../utils/logSanitize.js";
9
+ let provider;
10
+ let restoreConsole;
11
+ const queues = [];
12
+ /** Explicit opt-in; configuration never silently falls back to file logging. */
13
+ export function isProxyOtelOnly() {
14
+ return process.env.NEUROLINK_PROXY_LOG_SINK === "otel";
15
+ }
16
+ /** Reserve capacity including exports in flight, independently for metadata and bodies. */
17
+ function createTrackedProcessor(url, capacity) {
18
+ const state = {
19
+ attempted: 0,
20
+ submitted: 0,
21
+ transportAcknowledged: 0,
22
+ exportUnconfirmed: 0,
23
+ dropped: 0,
24
+ outstanding: 0,
25
+ lastAcknowledgedAt: undefined,
26
+ lastFailureAt: undefined,
27
+ };
28
+ const transport = new OTLPLogExporter({ url, timeoutMillis: 5000 });
29
+ const exporter = {
30
+ export(records, callback) {
31
+ let settled = false;
32
+ const settle = (result) => {
33
+ if (settled) {
34
+ return;
35
+ }
36
+ settled = true;
37
+ state.outstanding -= records.length;
38
+ if (result.code === ExportResultCode.SUCCESS) {
39
+ state.transportAcknowledged += records.length;
40
+ state.lastAcknowledgedAt = new Date().toISOString();
41
+ }
42
+ else {
43
+ state.exportUnconfirmed += records.length;
44
+ state.lastFailureAt = new Date().toISOString();
45
+ }
46
+ callback(result);
47
+ };
48
+ try {
49
+ transport.export(records, settle);
50
+ }
51
+ catch (error) {
52
+ settle({
53
+ code: ExportResultCode.FAILED,
54
+ error: error instanceof Error ? error : new Error(String(error)),
55
+ });
56
+ }
57
+ },
58
+ shutdown: () => transport.shutdown(),
59
+ };
60
+ const batch = new BatchLogRecordProcessor(exporter, {
61
+ maxQueueSize: capacity,
62
+ maxExportBatchSize: 64,
63
+ scheduledDelayMillis: 1000,
64
+ exportTimeoutMillis: 6000,
65
+ });
66
+ const processor = {
67
+ onEmit(record) {
68
+ state.attempted++;
69
+ if (state.outstanding >= capacity) {
70
+ state.dropped++;
71
+ return;
72
+ }
73
+ state.submitted++;
74
+ state.outstanding++;
75
+ batch.onEmit(record);
76
+ },
77
+ forceFlush: () => batch.forceFlush(),
78
+ shutdown: () => batch.shutdown(),
79
+ };
80
+ return { state, processor, capacity };
81
+ }
82
+ /** Initialize a log-only provider in every proxy process, including the supervisor. */
83
+ export function initializeProxyOtelLogs(role = "worker") {
84
+ if (!isProxyOtelOnly()) {
85
+ return undefined;
86
+ }
87
+ if (provider) {
88
+ return provider;
89
+ }
90
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ??
91
+ (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
92
+ ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, "")}/v1/logs`
93
+ : undefined);
94
+ if (!endpoint) {
95
+ throw new Error("OTel-only proxy logging requires an OTLP endpoint");
96
+ }
97
+ const url = new URL(endpoint);
98
+ if (!["http:", "https:"].includes(url.protocol)) {
99
+ throw new Error("Proxy OTLP logs endpoint must use HTTP or HTTPS");
100
+ }
101
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
102
+ if (url.protocol === "http:" && !loopback) {
103
+ throw new Error("Proxy OTLP logs require HTTPS for non-loopback collectors");
104
+ }
105
+ const metadata = createTrackedProcessor(endpoint, 2048);
106
+ const bodies = createTrackedProcessor(endpoint, 256);
107
+ queues.push(metadata, bodies);
108
+ provider = new LoggerProvider({
109
+ resource: resourceFromAttributes({
110
+ "service.name": process.env.OTEL_SERVICE_NAME ?? "neurolink-proxy",
111
+ "service.instance.id": `${role}-${process.pid}`,
112
+ "process.pid": process.pid,
113
+ "proxy.process.role": role,
114
+ }),
115
+ processors: [
116
+ {
117
+ onEmit(record, context) {
118
+ (record.attributes?.["proxy.record_kind"] === "body"
119
+ ? bodies
120
+ : metadata).processor.onEmit(record, context);
121
+ },
122
+ forceFlush: async () => {
123
+ await Promise.all(queues.map((q) => q.processor.forceFlush()));
124
+ },
125
+ shutdown: async () => {
126
+ await Promise.all(queues.map((q) => q.processor.shutdown()));
127
+ },
128
+ },
129
+ ],
130
+ });
131
+ return provider;
132
+ }
133
+ /** Structured evidence without final-request dashboard fields on auxiliary events. */
134
+ export function emitProxyOtelEvent(kind, record) {
135
+ if (!isProxyOtelOnly()) {
136
+ return;
137
+ }
138
+ try {
139
+ initializeProxyOtelLogs()
140
+ ?.getLogger("neurolink-proxy-events")
141
+ .emit({
142
+ severityNumber: SeverityNumber.INFO,
143
+ severityText: "INFO",
144
+ body: JSON.stringify(record),
145
+ attributes: {
146
+ "proxy.record_kind": kind,
147
+ "event.name": `proxy.${kind}`,
148
+ ...(typeof record.requestId === "string"
149
+ ? { "request.id": record.requestId }
150
+ : {}),
151
+ ...(typeof record.event === "string"
152
+ ? { "proxy.lifecycle.event": record.event }
153
+ : {}),
154
+ },
155
+ });
156
+ }
157
+ catch {
158
+ // Telemetry must never fail a model request. Invalid records are observable.
159
+ invalidRecords++;
160
+ }
161
+ }
162
+ let invalidRecords = 0;
163
+ /** Capture application console diagnostics only inside proxy service processes. */
164
+ export function routeProxyConsoleToOtel() {
165
+ if (!isProxyOtelOnly() || restoreConsole) {
166
+ return;
167
+ }
168
+ initializeProxyOtelLogs();
169
+ const originals = {
170
+ log: console.log,
171
+ info: console.info,
172
+ warn: console.warn,
173
+ error: console.error,
174
+ debug: console.debug,
175
+ };
176
+ let emitting = false;
177
+ for (const level of Object.keys(originals)) {
178
+ console[level] = (...args) => {
179
+ if (emitting) {
180
+ return;
181
+ }
182
+ emitting = true;
183
+ try {
184
+ const body = args
185
+ .map((value) => typeof value === "string"
186
+ ? value
187
+ : inspect(value, {
188
+ depth: 4,
189
+ maxArrayLength: 30,
190
+ maxStringLength: 16000,
191
+ }))
192
+ .join(" ");
193
+ provider?.getLogger("neurolink-proxy-console").emit({
194
+ body: sanitizeForLog(body, 32000),
195
+ severityText: level.toUpperCase(),
196
+ severityNumber: level === "error"
197
+ ? SeverityNumber.ERROR
198
+ : level === "warn"
199
+ ? SeverityNumber.WARN
200
+ : level === "debug"
201
+ ? SeverityNumber.DEBUG
202
+ : SeverityNumber.INFO,
203
+ attributes: { "proxy.record_kind": "console" },
204
+ });
205
+ }
206
+ catch {
207
+ invalidRecords++;
208
+ }
209
+ finally {
210
+ emitting = false;
211
+ }
212
+ };
213
+ }
214
+ restoreConsole = () => Object.assign(console, originals);
215
+ }
216
+ /** Counters acknowledge collector transport only, never backend persistence. */
217
+ export function getProxyOtelLogSnapshot() {
218
+ return {
219
+ mode: isProxyOtelOnly() ? "otel" : "file-and-otel",
220
+ initialized: provider !== undefined,
221
+ deliveryGuarantee: "best-effort; HTTP success is not per-record acceptance or backend persistence",
222
+ invalidRecords,
223
+ queues: queues.map((q, index) => ({
224
+ kind: index === 0 ? "metadata" : "bodies",
225
+ capacity: q.capacity,
226
+ ...q.state,
227
+ })),
228
+ };
229
+ }
230
+ /** Bounded provider flush belongs after final request and lifecycle publication. */
231
+ export async function flushProxyOtelLogs() {
232
+ await provider?.forceFlush();
233
+ }
234
+ /** Release this process's exporter and restore console ownership. */
235
+ export async function shutdownProxyOtelLogs() {
236
+ restoreConsole?.();
237
+ restoreConsole = undefined;
238
+ await provider?.shutdown();
239
+ provider = undefined;
240
+ queues.length = 0;
241
+ invalidRecords = 0;
242
+ }
243
+ /** Flush short-lived proxy command diagnostics on every normal return or exception. */
244
+ export function withProxyOtelLogShutdown(handler) {
245
+ return async (arg) => {
246
+ try {
247
+ await handler(arg);
248
+ }
249
+ finally {
250
+ await flushProxyOtelLogs().catch(() => undefined);
251
+ await shutdownProxyOtelLogs().catch(() => undefined);
252
+ }
253
+ };
254
+ }
@@ -1,3 +1,4 @@
1
+ import { emitProxyOtelEvent, isProxyOtelOnly, initializeProxyOtelLogs, } from "./otelLogSink.js";
1
2
  import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
2
3
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { appendFile } from "node:fs/promises";
@@ -297,6 +298,25 @@ export function configureProxyLifecycleLogger(options) {
297
298
  batchSize = positiveInteger(options.batchSize, DEFAULT_BATCH_SIZE);
298
299
  maxWriteRetries = positiveInteger(options.maxWriteRetries, DEFAULT_MAX_WRITE_RETRIES);
299
300
  flushIntervalMs = positiveInteger(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
301
+ if (options.enabled && isProxyOtelOnly()) {
302
+ initializeProxyOtelLogs(options.filePrefix === "proxy-supervisor" ? "supervisor" : "worker");
303
+ loggerEnabled = true;
304
+ sessionHashKey = process.env.NEUROLINK_PROXY_SESSION_SECRET
305
+ ? createHash("sha256")
306
+ .update(process.env.NEUROLINK_PROXY_SESSION_SECRET)
307
+ .digest()
308
+ : sessionHashKey;
309
+ stopRuntimeMetrics = startProxyRuntimeMetrics((runtimeSample) => {
310
+ logProxyLifecycleEvent({
311
+ event: "runtime_sample",
312
+ requestId: "-",
313
+ method: "-",
314
+ path: "-",
315
+ runtimeSample,
316
+ });
317
+ });
318
+ return;
319
+ }
300
320
  if (options.enabled && options.logDir) {
301
321
  try {
302
322
  mkdirSync(options.logDir, { recursive: true, mode: 0o700 });
@@ -336,7 +356,7 @@ export function logProxyLifecycleEvent(input) {
336
356
  * capacity is unavailable.
337
357
  */
338
358
  function enqueueLifecycleEvent(input, onPersisted) {
339
- if (!loggerEnabled || !lifecycleLogDir) {
359
+ if (!loggerEnabled || (!lifecycleLogDir && !isProxyOtelOnly())) {
340
360
  onPersisted?.(false);
341
361
  return;
342
362
  }
@@ -406,6 +426,10 @@ function enqueueLifecycleEvent(input, onPersisted) {
406
426
  : {}),
407
427
  ...(input.runtimeSample ? { runtimeSample: input.runtimeSample } : {}),
408
428
  };
429
+ if (isProxyOtelOnly()) {
430
+ emitProxyOtelEvent(filePrefix === "proxy-supervisor" ? "supervisor" : "lifecycle", record);
431
+ return;
432
+ }
409
433
  queue.push({
410
434
  filePrefix,
411
435
  logDir: lifecycleLogDir,
@@ -431,6 +455,10 @@ export async function persistProxyLifecycleAcceptance(input, timeoutMs = LIFECYC
431
455
  if (!loggerRequired) {
432
456
  return;
433
457
  }
458
+ if (isProxyOtelOnly()) {
459
+ enqueueLifecycleEvent({ ...input, event: "request_accepted" });
460
+ return;
461
+ }
434
462
  const confirmed = new Promise((resolve) => {
435
463
  enqueueLifecycleEvent({ ...input, event: "request_accepted" }, resolve);
436
464
  });
@@ -462,6 +490,8 @@ export async function flushProxyLifecycleEvents(timeoutMs = 5_000) {
462
490
  export function getProxyLifecycleLoggerSnapshot() {
463
491
  return {
464
492
  enabled: loggerEnabled,
493
+ sink: isProxyOtelOnly() ? "otel" : "file",
494
+ admissionPolicy: isProxyOtelOnly() ? "best-effort" : "durable-file",
465
495
  schemaVersion: SCHEMA_VERSION,
466
496
  processInstanceId,
467
497
  nextSequence,