@juspay/neurolink 12.14.6 → 12.14.8

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +411 -411
  3. package/dist/cli/commands/proxy.js +83 -14
  4. package/dist/proxy/bodyCaptureProcessing.js +47 -3
  5. package/dist/proxy/bodyCaptureWorker.js +39 -10
  6. package/dist/proxy/claudeOutputObservation.d.ts +9 -0
  7. package/dist/proxy/claudeOutputObservation.js +85 -0
  8. package/dist/proxy/codexUsage.js +86 -3
  9. package/dist/proxy/otelLogSink.d.ts +13 -0
  10. package/dist/proxy/otelLogSink.js +63 -2
  11. package/dist/proxy/processTelemetry.d.ts +3 -0
  12. package/dist/proxy/processTelemetry.js +25 -0
  13. package/dist/proxy/proxyTraceContext.d.ts +21 -0
  14. package/dist/proxy/proxyTraceContext.js +47 -0
  15. package/dist/proxy/proxyTracer.d.ts +9 -5
  16. package/dist/proxy/proxyTracer.js +81 -2
  17. package/dist/proxy/rawStreamCapture.js +17 -26
  18. package/dist/proxy/requestLogger.d.ts +2 -0
  19. package/dist/proxy/requestLogger.js +29 -19
  20. package/dist/proxy/restartControl.d.ts +3 -1
  21. package/dist/proxy/restartControl.js +118 -1
  22. package/dist/proxy/sseInterceptor.js +71 -3
  23. package/dist/server/routes/claudeProxyRoutes.d.ts +0 -11
  24. package/dist/server/routes/claudeProxyRoutes.js +27 -12
  25. package/dist/server/routes/codexProxyRoutes.js +496 -333
  26. package/dist/types/cli.d.ts +7 -1
  27. package/dist/types/proxy.d.ts +133 -3
  28. package/dist/types/proxyRestart.d.ts +25 -0
  29. package/dist/utils/logSanitize.js +1 -1
  30. package/docs-site/static/search-index.json +1 -1
  31. package/package.json +1 -1
  32. package/scripts/observability/check-proxy-telemetry.mjs +29 -241
  33. package/scripts/observability/proxy-telemetry-backend.mjs +202 -0
  34. package/scripts/observability/proxy-telemetry-check.mjs +578 -0
  35. package/scripts/observability/query-proxy-history.mjs +30 -19
@@ -0,0 +1,578 @@
1
+ /** Read-only coverage checks for stored OTLP telemetry, without local log scanning. */
2
+ import { createHash } from "node:crypto";
3
+ import { queryProxyHistory } from "./query-proxy-history.mjs";
4
+ import {
5
+ resolveProxyTelemetryBackend,
6
+ queryProxyTelemetry,
7
+ validateProxyTelemetryBackend,
8
+ } from "./proxy-telemetry-backend.mjs";
9
+
10
+ /** Empty traffic, unavailable evidence, and partial queries cannot become a green check.
11
+ * @param {import("../../src/lib/types/index.js").ProxyTelemetryDoctorOptions} options
12
+ */
13
+ export async function checkProxyTelemetry({
14
+ backend,
15
+ startTime,
16
+ endTime,
17
+ proxyUrl = "http://127.0.0.1:55669",
18
+ maxRows = 10000,
19
+ fetchImpl = fetch,
20
+ }) {
21
+ validateProxyTelemetryBackend(backend);
22
+ if (
23
+ !Number.isSafeInteger(startTime) ||
24
+ !Number.isSafeInteger(endTime) ||
25
+ startTime < 0 ||
26
+ startTime >= endTime ||
27
+ !Number.isSafeInteger(maxRows) ||
28
+ maxRows < 1 ||
29
+ maxRows > 100000
30
+ ) {
31
+ throw new Error(
32
+ "Provide an increasing microsecond time range and maxRows between 1 and 100000",
33
+ );
34
+ }
35
+ const budget = { used: 0, limit: 512 };
36
+ /** @type {import("../../src/lib/types/index.js").ProxyTelemetryCheck[]} */
37
+ const checks = [];
38
+ /** @type {unknown[]} */
39
+ const queries = [];
40
+ /** @param {string} name @param {import("../../src/lib/types/index.js").ProxyTelemetryCheck["status"]} status @param {unknown} evidence */
41
+ const add = (name, status, evidence) =>
42
+ checks.push({ name, status, evidence });
43
+ /** @param {string} sql @param {string} signal @param {Partial<import("../../src/lib/types/index.js").ProxyTelemetryQueryOptions>} options */
44
+ const query = async (sql, signal = "logs", options = {}) => {
45
+ if (budget.used >= budget.limit) {
46
+ throw new Error("Telemetry verification query budget reached");
47
+ }
48
+ budget.used++;
49
+ const result = await queryProxyTelemetry(
50
+ backend,
51
+ { sql, signal, startTime, endTime, ...options },
52
+ fetchImpl,
53
+ );
54
+ queries.push(result.query);
55
+ return result.rows;
56
+ };
57
+ const endpoint = new URL("/status", proxyUrl);
58
+ if (
59
+ endpoint.username ||
60
+ endpoint.password ||
61
+ (!(
62
+ ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) &&
63
+ endpoint.protocol === "http:"
64
+ ) &&
65
+ endpoint.protocol !== "https:")
66
+ ) {
67
+ throw new Error("Proxy diagnostics require HTTPS outside loopback");
68
+ }
69
+ const response = await fetchImpl(endpoint, {
70
+ redirect: "error",
71
+ signal: globalThis.AbortSignal.timeout(10000),
72
+ });
73
+ if (!response.ok) {
74
+ throw new Error(`Proxy status failed: HTTP ${response.status}`);
75
+ }
76
+ const runtime = await response.json(),
77
+ logs = runtime.observability?.requestLogs;
78
+ add(
79
+ "runtime",
80
+ runtime.ready && runtime.acceptingConnections ? "pass" : "fail",
81
+ { pid: runtime.pid, version: runtime.version, ready: runtime.ready },
82
+ );
83
+ add(
84
+ "otel_logging",
85
+ logs?.otel?.initialized && logs.diskEnabled === false ? "pass" : "fail",
86
+ { initialized: logs?.otel?.initialized, diskEnabled: logs?.diskEnabled },
87
+ );
88
+ const supervisor = runtime.observability?.supervisor;
89
+ const supervisorProcess = supervisor?.process;
90
+ const workerProcess = runtime.observability?.process;
91
+ /** @param {import("../../src/lib/types/index.js").ProxyProcessTelemetrySnapshot | undefined} process */
92
+ const otelProcess = (process) => process?.configuredSink === "otel" &&
93
+ process.lifecycleSink === "otel" && process.otelInitialized &&
94
+ process.stdio?.stdout === "non_file" && process.stdio?.stderr === "non_file";
95
+ add("worker_process_logging", !workerProcess ? "unverified" : otelProcess(workerProcess) ? "pass" : "fail", workerProcess ?? { reason: "process_telemetry_missing" });
96
+ add("supervisor_logging",
97
+ supervisor?.status === "not_applicable" && !runtime.autoUpdate?.supervisorPid ? "pass" :
98
+ supervisor?.status !== "available" ? "unverified" :
99
+ supervisorProcess?.pid === runtime.autoUpdate?.supervisorPid && otelProcess(supervisorProcess) ? "pass" : "fail",
100
+ supervisor ?? { status: "unavailable", reason: "supervisor_telemetry_missing" });
101
+ /** @type {Array<{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}>} */
102
+ const failures = (logs?.otel?.queues ?? []).map(
103
+ (
104
+ /** @type {{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}} */ q,
105
+ ) => ({
106
+ kind: q.kind,
107
+ dropped: q.dropped,
108
+ exportUnconfirmed: q.exportUnconfirmed,
109
+ outstanding: q.outstanding,
110
+ capacity: q.capacity,
111
+ recentFailures: q.recentFailures,
112
+ failureHistoryEvicted: q.failureHistoryEvicted,
113
+ }),
114
+ );
115
+ add(
116
+ "producer_delivery",
117
+ ["metadata", "bodies"].every((kind) =>
118
+ failures.some((q) => q.kind === kind),
119
+ ) &&
120
+ failures.every(
121
+ (q) =>
122
+ q.dropped === 0 &&
123
+ q.exportUnconfirmed === 0 &&
124
+ q.outstanding < q.capacity,
125
+ )
126
+ ? "pass"
127
+ : "warn",
128
+ failures,
129
+ );
130
+ add(
131
+ "capture_admission",
132
+ logs?.bodyCapture &&
133
+ logs.bodyCapture.rejected === 0 &&
134
+ logs.bodyCapture.failed === 0
135
+ ? "pass"
136
+ : "warn",
137
+ logs?.bodyCapture ?? { status: "unavailable" },
138
+ );
139
+ for (const [signal, table] of [
140
+ ["logs", backend.stream],
141
+ ["traces", backend.stream],
142
+ ["metrics", "proxy_requests_total"],
143
+ ]) {
144
+ const [value] = await query(
145
+ `SELECT COUNT(*) AS records, MAX(_timestamp) AS latest FROM "${table}"`,
146
+ signal,
147
+ );
148
+ add(
149
+ `${signal}_freshness`,
150
+ Number(value?.records) > 0 &&
151
+ Number.isFinite(Number(value?.latest)) &&
152
+ Number(value.latest) <= endTime &&
153
+ (endTime - Number(value.latest)) / 1e6 <= 120
154
+ ? "pass"
155
+ : "unverified",
156
+ {
157
+ maxLagAtWindowEndSeconds: 120,
158
+ records: value?.records ?? 0,
159
+ latest: value?.latest,
160
+ ageAtWindowEndSeconds: value?.latest
161
+ ? (endTime - Number(value.latest)) / 1e6
162
+ : null,
163
+ },
164
+ );
165
+ }
166
+ /** @type {Record<string, import("../../src/lib/types/index.js").ProxyTelemetryStoredRecord[]>} */
167
+ const history = Object.create(null);
168
+ for (const kind of ["request_final", "body_capture_index", "lifecycle"]) {
169
+ const result = await queryProxyHistory({
170
+ ...backend,
171
+ // Capture publication is asynchronous. Include its bounded settling
172
+ // window, and a request-start margin, when reconciling phases to finals.
173
+ startTime: kind === "body_capture_index" ? Math.max(0, startTime - 120e6) : startTime,
174
+ endTime: kind === "body_capture_index" ? Math.min(Date.now() * 1000, endTime + 120e6) : endTime,
175
+ kind,
176
+ maxRows,
177
+ fetchImpl,
178
+ budget,
179
+ });
180
+ queries.push(...result.queries.map((q) => ({ ...q, kind })));
181
+ history[kind] = result.records.map((row) => {
182
+ if (typeof row.body !== "string") {
183
+ throw new Error("Invalid stored metadata body");
184
+ }
185
+ const value = JSON.parse(row.body);
186
+ if (
187
+ !value ||
188
+ typeof value !== "object" ||
189
+ typeof value.requestId !== "string"
190
+ ) {
191
+ throw new Error("Stored telemetry metadata has an invalid schema");
192
+ }
193
+ return { ...value, recordedAtMicroseconds: Number(row._timestamp) };
194
+ });
195
+ }
196
+ const finals = history.request_final,
197
+ unique = new Set(finals.map((r) => r.requestId));
198
+ add(
199
+ "final_uniqueness",
200
+ !finals.length
201
+ ? "unverified"
202
+ : unique.size === finals.length
203
+ ? "pass"
204
+ : "fail",
205
+ { records: finals.length, uniqueRequestIds: unique.size },
206
+ );
207
+ const terminalEvents = history.lifecycle.filter(
208
+ (row) => row.event === "request_terminal",
209
+ );
210
+ const missingFinals = terminalEvents.filter(
211
+ (row) =>
212
+ (row.telemetryStatus !== undefined &&
213
+ row.telemetryStatus !== "complete") ||
214
+ ((row.outcomeSource === "final_request" ||
215
+ row.outcomeSource === undefined) &&
216
+ !unique.has(row.requestId)),
217
+ );
218
+ add(
219
+ "terminal_reconciliation",
220
+ !terminalEvents.length
221
+ ? "unverified"
222
+ : missingFinals.length
223
+ ? "fail"
224
+ : "pass",
225
+ {
226
+ terminals: terminalEvents.length,
227
+ incompleteOrMissingFinal: missingFinals.length,
228
+ requestIds: missingFinals.slice(0, 50).map((row) => row.requestId),
229
+ boundary:
230
+ "Terminals inside the selected interval; admissions still in flight are not failures",
231
+ },
232
+ );
233
+ /** @type {Record<string, import("../../src/lib/types/index.js").ProxyTelemetryFieldCoverage>} */
234
+ const coverage = Object.create(null);
235
+ for (const row of finals) {
236
+ const group = (coverage[row.model ?? "unknown"] ??= {
237
+ records: 0,
238
+ traceMissing: 0,
239
+ durationMissing: 0,
240
+ outcomeMissing: 0,
241
+ firstOutputUnexplained: 0,
242
+ });
243
+ group.records++;
244
+ if (
245
+ !/^[a-f0-9]{32}$/i.test(row.traceId ?? "") ||
246
+ /^0+$/.test(row.traceId ?? "")
247
+ ) {
248
+ group.traceMissing++;
249
+ }
250
+ if (
251
+ !Number.isFinite(row.responseTimeMs) ||
252
+ (row.responseTimeMs ?? -1) < 0
253
+ ) {
254
+ group.durationMissing++;
255
+ }
256
+ if (
257
+ ![
258
+ "completed",
259
+ "bodyless",
260
+ "client_cancelled",
261
+ "stream_error",
262
+ "handler_error",
263
+ ].includes(row.terminalOutcome ?? "")
264
+ ) {
265
+ group.outcomeMissing++;
266
+ }
267
+ const timing = row.firstUsefulOutputMs;
268
+ const validTiming =
269
+ Number.isFinite(timing) &&
270
+ (timing ?? -1) >= 0 &&
271
+ (timing ?? Infinity) <= (row.responseTimeMs ?? -1);
272
+ const knownAbsent =
273
+ row.firstUsefulOutputStatus === "no_useful_output" &&
274
+ timing === undefined &&
275
+ row.firstUsefulOutputEvent === undefined;
276
+ const validAvailability =
277
+ row.firstUsefulOutputStatus === undefined ||
278
+ ["observed", "no_useful_output", "not_observed"].includes(
279
+ row.firstUsefulOutputStatus,
280
+ );
281
+ if (
282
+ !validAvailability ||
283
+ (timing !== undefined && !validTiming) ||
284
+ (row.firstUsefulOutputStatus === "observed" && !validTiming) ||
285
+ (row.firstUsefulOutputStatus === "no_useful_output" && !knownAbsent) ||
286
+ (row.terminalOutcome === "completed" &&
287
+ !(validTiming && row.firstUsefulOutputStatus !== "not_observed") &&
288
+ !knownAbsent)
289
+ ) {
290
+ group.firstOutputUnexplained++;
291
+ }
292
+ }
293
+ add(
294
+ "request_field_coverage",
295
+ !finals.length
296
+ ? "unverified"
297
+ : Object.values(coverage).every(
298
+ (g) =>
299
+ g.traceMissing +
300
+ g.durationMissing +
301
+ g.outcomeMissing +
302
+ g.firstOutputUnexplained ===
303
+ 0,
304
+ )
305
+ ? "pass"
306
+ : "fail",
307
+ coverage,
308
+ );
309
+ const indexes = history.body_capture_index.filter((row) =>
310
+ (row.recordedAtMicroseconds ?? -1) >= startTime &&
311
+ (row.recordedAtMicroseconds ?? Infinity) < endTime);
312
+ /** @type {Map<string | undefined, Set<string | undefined>>} */
313
+ const phasesByRequest = new Map();
314
+ for (const index of history.body_capture_index) {
315
+ let phases = phasesByRequest.get(index.requestId);
316
+ if (!phases) { phases = new Set(); phasesByRequest.set(index.requestId, phases); }
317
+ phases.add(index.phase);
318
+ }
319
+ const captureFinals = finals.filter((row) => row.path && ["/v1/messages", "/backend-api/codex/responses", "/v1/responses"].includes(row.path));
320
+ const missingCapturePhases = captureFinals.flatMap((row) => {
321
+ // A request may have started outside the selected interval. Its response
322
+ // capture still has to be present when this final is inside the interval.
323
+ return phasesByRequest.get(row.requestId)?.has("client_response") ? [] : [{ requestId: row.requestId, missingPhase: "client_response" }];
324
+ });
325
+ add("request_capture_coverage", !captureFinals.length ? "unverified" : missingCapturePhases.length ? "fail" : "pass", {
326
+ finals: finals.length, eligibleFinals: captureFinals.length, missing: missingCapturePhases.length,
327
+ examples: missingCapturePhases.slice(0, 50), omitted: Math.max(0, missingCapturePhases.length - 50),
328
+ });
329
+ /** @type {Record<string, number>} */
330
+ const byDelivery = Object.create(null);
331
+ for (const row of indexes) {
332
+ const status = row.bodyDelivery?.status ?? "missing";
333
+ byDelivery[status] = (byDelivery[status] ?? 0) + 1;
334
+ }
335
+ const captureFailures = indexes.filter(
336
+ (row) =>
337
+ row.captureError ||
338
+ row.bodyTruncated ||
339
+ !["transport_acknowledged", "policy_excluded", "no_body"].includes(
340
+ row.bodyDelivery?.status ?? "",
341
+ ),
342
+ );
343
+ add(
344
+ "capture_delivery",
345
+ !indexes.length ? "unverified" : captureFailures.length ? "fail" : "pass",
346
+ {
347
+ records: indexes.length,
348
+ byDelivery,
349
+ failures: captureFailures.slice(0, 50).map((row) => ({
350
+ requestId: row.requestId,
351
+ captureId: row.captureId,
352
+ phase: row.phase,
353
+ captureError: row.captureError,
354
+ captureAdmission: row.captureAdmission,
355
+ bodyTruncated: row.bodyTruncated,
356
+ bodyDelivery: row.bodyDelivery,
357
+ })),
358
+ failuresOmitted: Math.max(0, captureFailures.length - 50),
359
+ },
360
+ );
361
+ const bodyChecks = [];
362
+ for (const index of indexes
363
+ .filter(
364
+ (row) =>
365
+ row.bodySha256 && row.bodyDelivery?.status === "transport_acknowledged",
366
+ )
367
+ .sort((a, b) => (b.redactedBodyBytes ?? 0) - (a.redactedBodyBytes ?? 0))
368
+ .slice(0, 3)) {
369
+ if (!/^[a-f0-9-]{36}$/i.test(index.captureId ?? "")) {
370
+ throw new Error("Invalid capture identifier");
371
+ }
372
+ if (
373
+ !Number.isSafeInteger(index.redactedBodyBytes) ||
374
+ (index.redactedBodyBytes ?? -1) < 0 ||
375
+ (index.redactedBodyBytes ?? 0) > 8 * 1024 * 1024
376
+ ) {
377
+ throw new Error("Capture verification exceeds the 8 MiB per-body bound");
378
+ }
379
+ const chunks = await query(
380
+ `SELECT body_chunk_index, body_chunk_count, body FROM "${backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${index.captureId}' ORDER BY body_chunk_index ASC`,
381
+ "logs",
382
+ {
383
+ startTime: startTime - 120e6,
384
+ endTime: Math.min(Date.now() * 1000, endTime + 120e6),
385
+ size: 1000,
386
+ },
387
+ );
388
+ if (chunks.length >= 1000) {
389
+ throw new Error("Capture verification exceeded its chunk budget");
390
+ }
391
+ const bytes = chunks.reduce(
392
+ (sum, r) =>
393
+ sum +
394
+ (typeof r.body === "string" ? Buffer.byteLength(r.body) : Infinity),
395
+ 0,
396
+ );
397
+ if (bytes > 8 * 1024 * 1024) {
398
+ throw new Error("Stored capture exceeds the 8 MiB verification bound");
399
+ }
400
+ const raw = chunks.map((r) => r.body).join(""),
401
+ expected = Number(chunks[0]?.body_chunk_count ?? 0);
402
+ const contiguous = chunks.every(
403
+ (row, i) =>
404
+ Number(row.body_chunk_index) === i &&
405
+ Number(row.body_chunk_count) === expected,
406
+ );
407
+ const actualSha256 = createHash("sha256").update(raw).digest("hex");
408
+ bodyChecks.push({
409
+ captureId: index.captureId,
410
+ expectedChunks: expected,
411
+ storedChunks: chunks.length,
412
+ verified:
413
+ expected > 0 &&
414
+ expected === chunks.length &&
415
+ contiguous &&
416
+ Buffer.byteLength(raw) === index.redactedBodyBytes &&
417
+ actualSha256 === index.bodySha256,
418
+ });
419
+ }
420
+ add(
421
+ "sample_body_integrity",
422
+ !bodyChecks.length
423
+ ? "unverified"
424
+ : bodyChecks.every((r) => r.verified)
425
+ ? "pass"
426
+ : "fail",
427
+ bodyChecks,
428
+ );
429
+ const traceIds = [
430
+ ...new Set(
431
+ finals
432
+ .map((row) => row.traceId)
433
+ .filter((id) => /^[a-f0-9]{32}$/i.test(id ?? "")),
434
+ ),
435
+ ].slice(-3);
436
+ const correlated = traceIds.length
437
+ ? await query(
438
+ `SELECT trace_id, COUNT(*) AS spans FROM "${backend.stream}" WHERE trace_id IN (${traceIds.map((id) => `'${id}'`).join(",")}) GROUP BY trace_id`,
439
+ "traces",
440
+ {
441
+ startTime: startTime - 120e6,
442
+ endTime: Math.min(Date.now() * 1000, endTime + 120e6),
443
+ },
444
+ )
445
+ : [];
446
+ add(
447
+ "sample_trace_correlation",
448
+ !traceIds.length
449
+ ? "unverified"
450
+ : traceIds.every((id) =>
451
+ correlated.some(
452
+ (row) => row.trace_id === id && Number(row.spans) > 0,
453
+ ),
454
+ )
455
+ ? "pass"
456
+ : "fail",
457
+ { requested: traceIds.length, found: correlated.length },
458
+ );
459
+ if (backend.collectorMetricsUrl) {
460
+ const url = new URL(backend.collectorMetricsUrl);
461
+ if (
462
+ !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
463
+ !["http:", "https:"].includes(url.protocol)
464
+ ) {
465
+ throw new Error("Collector diagnostics must use loopback HTTP(S)");
466
+ }
467
+ const metrics = await fetchImpl(url, {
468
+ redirect: "error",
469
+ signal: globalThis.AbortSignal.timeout(10000),
470
+ });
471
+ if (!metrics.ok) {
472
+ throw new Error(`Collector diagnostics failed: HTTP ${metrics.status}`);
473
+ }
474
+ const counters = Object.fromEntries(
475
+ (await metrics.text())
476
+ .split("\n")
477
+ .filter((line) =>
478
+ /^otelcol_(exporter_(queue_size|sent|send_failed|enqueue_failed)|receiver_(refused|failed))/.test(
479
+ line,
480
+ ),
481
+ )
482
+ .map((line) => {
483
+ const match = /^(\w+(?:\{.*\})?)\s+(\S+)(?:\s+\d+)?$/.exec(line);
484
+ return [match?.[1] ?? "invalid_sample", Number(match?.[2])];
485
+ }),
486
+ );
487
+ add(
488
+ "collector_delivery",
489
+ ["log_records", "spans", "metric_points"].every((signal) =>
490
+ Object.keys(counters).some(
491
+ (key) =>
492
+ key.startsWith(`otelcol_exporter_send_failed_${signal}`) ||
493
+ key.startsWith(`otelcol_exporter_sent_${signal}`),
494
+ ),
495
+ )
496
+ ? Object.entries(counters).some(
497
+ ([key, value]) =>
498
+ !Number.isFinite(value) ||
499
+ value < 0 ||
500
+ (/^otelcol_(exporter_(send_failed|enqueue_failed)|receiver_(refused|failed))/.test(
501
+ key,
502
+ ) &&
503
+ value > 0),
504
+ )
505
+ ? "warn"
506
+ : "pass"
507
+ : "unverified",
508
+ {
509
+ scope:
510
+ "collector lifetime delivery/failure counters; absent failure series are not required when sent series identify that signal; queue_size is an instantaneous gauge and a nonzero queue alone is not a failure",
511
+ counters,
512
+ },
513
+ );
514
+ } else {
515
+ add("collector_delivery", "unverified", {
516
+ reason: "collector metrics endpoint not configured",
517
+ });
518
+ }
519
+ return {
520
+ schemaVersion: 1,
521
+ checkedAt: new Date().toISOString(),
522
+ startTime,
523
+ endTimeExclusive: endTime,
524
+ source: "OpenObserve query API over stored OTLP telemetry",
525
+ completeQuery: true,
526
+ status: checks.some((c) => c.status === "fail")
527
+ ? "fail"
528
+ : checks.some((c) => c.status !== "pass")
529
+ ? "incomplete"
530
+ : "pass",
531
+ guarantee:
532
+ "Coverage and bounded sample verification, not exactly-once or universal lossless delivery",
533
+ checks,
534
+ queries,
535
+ };
536
+ }
537
+
538
+ /** Shipped CLI and repository doctor use the same implementation. */
539
+ export async function runProxyTelemetryDoctor() {
540
+ const args = process.argv.slice(2);
541
+ /** @param {string} name */
542
+ const value = (name) => {
543
+ const i = args.indexOf(name);
544
+ return i < 0 ? undefined : args[i + 1];
545
+ };
546
+ if (args.includes("--help")) {
547
+ console.log(
548
+ "Usage: neurolink proxy telemetry doctor [--since ISO_DATE] [--until ISO_DATE] [--format json|text] [--proxy-url URL] [--max-rows 10000]",
549
+ );
550
+ return;
551
+ }
552
+ const until = value("--until")
553
+ ? Date.parse(value("--until") ?? "")
554
+ : Date.now() - 30000;
555
+ const since = value("--since")
556
+ ? Date.parse(value("--since") ?? "")
557
+ : until - 15 * 60000;
558
+ const report = await checkProxyTelemetry({
559
+ backend: await resolveProxyTelemetryBackend(),
560
+ startTime: since * 1000,
561
+ endTime: until * 1000,
562
+ proxyUrl: value("--proxy-url") ?? process.env.NEUROLINK_PROXY_URL,
563
+ maxRows: Number(value("--max-rows") ?? 10000),
564
+ });
565
+ if (value("--format") === "json") {
566
+ console.log(JSON.stringify(report, null, 2));
567
+ } else {
568
+ console.log(`NeuroLink OTel coverage: ${report.status}`);
569
+ for (const check of report.checks) {
570
+ console.log(
571
+ `${check.status.padEnd(10)} ${check.name}: ${JSON.stringify(check.evidence)}`,
572
+ );
573
+ }
574
+ }
575
+ if (report.status !== "pass") {
576
+ process.exitCode = 1;
577
+ }
578
+ }
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /** Bounded, deterministic OpenObserve history queries; never accept partial data. */
3
3
  import { pathToFileURL } from "node:url";
4
+ import {
5
+ resolveProxyTelemetryBackend,
6
+ validateProxyTelemetryBackend,
7
+ } from "./proxy-telemetry-backend.mjs";
4
8
 
5
9
  const KINDS = new Set([
6
10
  "request_final",
@@ -9,11 +13,13 @@ const KINDS = new Set([
9
13
  "supervisor",
10
14
  "body_capture_index",
11
15
  "stream_error",
16
+ "telemetry_delivery",
17
+ "console",
12
18
  ]);
13
19
 
14
20
  /**
15
21
  * Query metadata in small windows; body chunks require a targeted lookup.
16
- * @param {{ baseUrl: string, organization?: string, stream?: string, authorization?: string, startTime: number, endTime: number, kind?: string, maxRows?: number, fetchImpl?: typeof fetch }} options
22
+ * @param {{ baseUrl: string, organization?: string, stream?: string, authorization?: string, startTime: number, endTime: number, kind?: string, maxRows?: number, budget?: import("../../src/lib/types/index.js").ProxyTelemetryQueryBudget, fetchImpl?: typeof fetch }} options
17
23
  */
18
24
  export async function queryProxyHistory({
19
25
  baseUrl,
@@ -25,6 +31,7 @@ export async function queryProxyHistory({
25
31
  kind = "request_final",
26
32
  maxRows = 10_000,
27
33
  fetchImpl = fetch,
34
+ budget = { used: 0, limit: 512 },
28
35
  }) {
29
36
  if (
30
37
  !/^[a-zA-Z0-9_-]+$/.test(organization) ||
@@ -45,12 +52,24 @@ export async function queryProxyHistory({
45
52
  "Provide an increasing microsecond time range and maxRows between 1 and 100000",
46
53
  );
47
54
  }
55
+ validateProxyTelemetryBackend({
56
+ baseUrl,
57
+ organization,
58
+ stream,
59
+ authorization,
60
+ });
48
61
  const endpoint = new URL(`/api/${organization}/_search?type=logs`, baseUrl);
49
- const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
50
- if (authorization && endpoint.protocol !== "https:" && !(
51
- endpoint.protocol === "http:" && loopback
52
- )) {
53
- throw new Error("Credentialed OpenObserve queries require HTTPS outside loopback");
62
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(
63
+ endpoint.hostname,
64
+ );
65
+ if (
66
+ authorization &&
67
+ endpoint.protocol !== "https:" &&
68
+ !(endpoint.protocol === "http:" && loopback)
69
+ ) {
70
+ throw new Error(
71
+ "Credentialed OpenObserve queries require HTTPS outside loopback",
72
+ );
54
73
  }
55
74
  /** @type {Array<{start: number, endExclusive: number, offset: number, partial: boolean, tookMs?: number}>} */
56
75
  const queries = [];
@@ -59,14 +78,15 @@ export async function queryProxyHistory({
59
78
  async function readWindow(start, end) {
60
79
  const rows = [];
61
80
  for (let offset = 0; ; offset += 200) {
62
- if (queries.length >= 512) {
81
+ if (budget.used >= budget.limit) {
63
82
  throw new Error(
64
83
  "History exceeds the 512-query budget; narrow the interval",
65
84
  );
66
85
  }
86
+ budget.used++;
67
87
  const response = await fetchImpl(endpoint, {
68
88
  method: "POST",
69
- redirect: authorization ? "error" : "follow",
89
+ redirect: "error",
70
90
  signal: globalThis.AbortSignal.timeout(30_000),
71
91
  headers: {
72
92
  "Content-Type": "application/json",
@@ -162,18 +182,9 @@ async function main() {
162
182
  const since = Date.parse(value("--since") ?? "");
163
183
  const untilValue = value("--until");
164
184
  const until = untilValue ? Date.parse(untilValue) : Date.now();
165
- const user = process.env.NEUROLINK_OPENOBSERVE_USER;
166
- const password = process.env.NEUROLINK_OPENOBSERVE_PASSWORD;
167
- const authorization =
168
- process.env.NEUROLINK_OPENOBSERVE_BASIC_AUTH ??
169
- (user && password
170
- ? `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`
171
- : undefined);
185
+ const backend = await resolveProxyTelemetryBackend();
172
186
  const report = await queryProxyHistory({
173
- baseUrl: process.env.NEUROLINK_OPENOBSERVE_URL ?? "http://127.0.0.1:5080",
174
- organization: process.env.NEUROLINK_OPENOBSERVE_ORG ?? "default",
175
- stream: process.env.NEUROLINK_PROXY_STREAM_HEADER ?? "neurolink_proxy",
176
- authorization,
187
+ ...backend,
177
188
  startTime: since * 1000,
178
189
  endTime: until * 1000,
179
190
  kind: value("--kind") ?? "request_final",