@juspay/neurolink 12.14.5 → 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.d.ts +1 -1
- package/dist/cli/commands/proxy.js +123 -14
- 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/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/proxy/restartControl.d.ts +12 -0
- package/dist/proxy/restartControl.js +283 -0
- package/dist/proxy/rollingWorkerSupervisor.js +8 -0
- package/dist/server/routes/codexProxyRoutes.js +414 -332
- package/dist/types/cli.d.ts +7 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxy.d.ts +119 -3
- 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 +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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.14.
|
|
3
|
+
"version": "12.14.7",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
"test:proxy": "pnpm exec tsx test/continuous-test-suite-proxy.ts",
|
|
148
148
|
"test:proxy-connect-retry": "pnpm exec tsx test/continuous-test-suite-proxy-connect-retry.ts",
|
|
149
149
|
"test:proxy-telemetry": "pnpm exec tsx test/continuous-test-suite-proxy-telemetry.ts",
|
|
150
|
+
"test:proxy-restart": "pnpm exec tsx test/continuous-test-suite-proxy-restart.ts",
|
|
150
151
|
"test:codex": "pnpm exec tsx test/continuous-test-suite-codex.ts",
|
|
151
152
|
"test:bugfixes": "pnpm exec tsx test/continuous-test-suite-bugfixes.ts",
|
|
152
153
|
"test:json-e2e": "pnpm exec tsx test/continuous-test-suite-json-e2e.ts",
|
|
@@ -1,248 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const STREAMS_TO_CHECK = [
|
|
14
|
-
{
|
|
15
|
-
type: "logs",
|
|
16
|
-
name: process.env.NEUROLINK_PROXY_STREAM_HEADER || "neurolink_proxy",
|
|
17
|
-
label: "OpenObserve logs",
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
type: "traces",
|
|
21
|
-
name: process.env.NEUROLINK_PROXY_STREAM_HEADER || "neurolink_proxy",
|
|
22
|
-
label: "OpenObserve traces",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
type: "metrics",
|
|
26
|
-
name: "proxy_requests_total",
|
|
27
|
-
label: "OpenObserve proxy metrics",
|
|
28
|
-
},
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
function getAuthHeader() {
|
|
32
|
-
if (process.env.NEUROLINK_OPENOBSERVE_BASIC_AUTH) {
|
|
33
|
-
return process.env.NEUROLINK_OPENOBSERVE_BASIC_AUTH;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const user =
|
|
37
|
-
process.env.NEUROLINK_OPENOBSERVE_USER || DEFAULT_OPENOBSERVE_USER;
|
|
38
|
-
const password =
|
|
39
|
-
process.env.NEUROLINK_OPENOBSERVE_PASSWORD || DEFAULT_OPENOBSERVE_PASSWORD;
|
|
40
|
-
|
|
41
|
-
return `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function formatDate(micros) {
|
|
45
|
-
return new Date(Math.floor(micros / 1000)).toISOString();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatAgeSeconds(seconds) {
|
|
49
|
-
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
50
|
-
return "n/a";
|
|
51
|
-
}
|
|
52
|
-
if (seconds < 60) {
|
|
53
|
-
return `${Math.round(seconds)}s`;
|
|
54
|
-
}
|
|
55
|
-
const minutes = seconds / 60;
|
|
56
|
-
if (minutes < 60) {
|
|
57
|
-
return `${Math.round(minutes)}m`;
|
|
58
|
-
}
|
|
59
|
-
const hours = minutes / 60;
|
|
60
|
-
return `${hours.toFixed(1)}h`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function summarizeLag(ageSeconds, thresholdSeconds) {
|
|
64
|
-
if (!Number.isFinite(ageSeconds)) {
|
|
65
|
-
return "missing";
|
|
66
|
-
}
|
|
67
|
-
return ageSeconds <= thresholdSeconds ? "fresh" : "stale";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function fetchStreams(type) {
|
|
71
|
-
const url = new URL(
|
|
72
|
-
`/api/${process.env.NEUROLINK_OPENOBSERVE_ORG || DEFAULT_OPENOBSERVE_ORG}/streams?type=${type}`,
|
|
73
|
-
process.env.NEUROLINK_OPENOBSERVE_URL || DEFAULT_OPENOBSERVE_URL,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const response = await fetch(url, {
|
|
77
|
-
signal: globalThis.AbortSignal.timeout(15_000),
|
|
78
|
-
headers: {
|
|
79
|
-
Authorization: getAuthHeader(),
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!response.ok) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Failed to fetch ${type} streams: ${response.status} ${response.statusText}`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const body = await response.json();
|
|
90
|
-
return body.list || [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function readLatestLocalSummary() {
|
|
94
|
-
const logsDir = join(homedir(), ".neurolink", "logs");
|
|
95
|
-
let entries;
|
|
96
|
-
try {
|
|
97
|
-
entries = await fs.readdir(logsDir);
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (err.code === "ENOENT") {
|
|
100
|
-
return null; // No log directory yet — fresh setup
|
|
101
|
-
}
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
const files = entries
|
|
105
|
-
.filter((name) => /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name))
|
|
106
|
-
.sort()
|
|
107
|
-
.reverse();
|
|
108
|
-
|
|
109
|
-
for (const name of files) {
|
|
110
|
-
const path = join(logsDir, name);
|
|
111
|
-
let handle;
|
|
112
|
-
try {
|
|
113
|
-
handle = await fs.open(path, "r");
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (err.code === "ENOENT") {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const stats = await handle.stat();
|
|
122
|
-
const chunkSize = Math.min(stats.size, 64 * 1024);
|
|
123
|
-
const buffer = Buffer.alloc(chunkSize);
|
|
124
|
-
await handle.read(buffer, 0, chunkSize, stats.size - chunkSize);
|
|
125
|
-
const lines = buffer
|
|
126
|
-
.toString("utf8")
|
|
127
|
-
.split("\n")
|
|
128
|
-
.map((line) => line.trim())
|
|
129
|
-
.filter(Boolean);
|
|
130
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(lines[i]);
|
|
133
|
-
} catch {
|
|
134
|
-
// ignore malformed tail line
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
await handle.close();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function main() {
|
|
146
|
-
const thresholdSeconds = Number(
|
|
147
|
-
process.env.NEUROLINK_PROXY_TELEMETRY_MAX_LAG_SEC ||
|
|
148
|
-
DEFAULT_MAX_LAG_SECONDS,
|
|
149
|
-
);
|
|
150
|
-
const nowMicros = Date.now() * 1000;
|
|
151
|
-
|
|
152
|
-
const groupedStreams = Object.fromEntries(
|
|
153
|
-
await Promise.all(
|
|
154
|
-
[...new Set(STREAMS_TO_CHECK.map((stream) => stream.type))].map(
|
|
155
|
-
async (type) => [type, await fetchStreams(type)],
|
|
156
|
-
),
|
|
157
|
-
),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const otelOnly = process.env.NEUROLINK_PROXY_LOG_SINK === "otel";
|
|
161
|
-
const localSummary = otelOnly ? null : await readLatestLocalSummary();
|
|
162
|
-
const localSummaryMicros = localSummary?.timestamp
|
|
163
|
-
? new Date(localSummary.timestamp).getTime() * 1000
|
|
164
|
-
: null;
|
|
165
|
-
|
|
166
|
-
console.log("NeuroLink Proxy Telemetry Doctor");
|
|
167
|
-
console.log("================================");
|
|
168
|
-
console.log(
|
|
169
|
-
`OpenObserve: ${process.env.NEUROLINK_OPENOBSERVE_URL || DEFAULT_OPENOBSERVE_URL}`,
|
|
170
|
-
);
|
|
171
|
-
console.log(`Lag threshold: ${thresholdSeconds}s`);
|
|
172
|
-
console.log("");
|
|
173
|
-
|
|
174
|
-
let hasProblem = false;
|
|
175
|
-
|
|
176
|
-
for (const stream of STREAMS_TO_CHECK) {
|
|
177
|
-
const item = groupedStreams[stream.type]?.find(
|
|
178
|
-
(entry) => entry.name === stream.name,
|
|
179
|
-
);
|
|
180
|
-
const latestMicros = item?.stats?.doc_time_max ?? null;
|
|
181
|
-
const ageSeconds = latestMicros
|
|
182
|
-
? Math.max(0, (nowMicros - latestMicros) / 1_000_000)
|
|
183
|
-
: Number.NaN;
|
|
184
|
-
const status = summarizeLag(ageSeconds, thresholdSeconds);
|
|
185
|
-
if (status !== "fresh") {
|
|
186
|
-
hasProblem = true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
console.log(`${stream.label}: ${status}`);
|
|
190
|
-
console.log(` stream: ${stream.name}`);
|
|
191
|
-
console.log(
|
|
192
|
-
` latest: ${latestMicros ? formatDate(latestMicros) : "missing"}`,
|
|
193
|
-
);
|
|
194
|
-
console.log(` age: ${formatAgeSeconds(ageSeconds)}`);
|
|
195
|
-
console.log(` docs: ${item?.stats?.doc_num ?? 0}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log("");
|
|
199
|
-
console.log("Local proxy summary log:");
|
|
200
|
-
if (otelOnly) {
|
|
2
|
+
import { runProxyTelemetryDoctor } from "./proxy-telemetry-check.mjs";
|
|
3
|
+
runProxyTelemetryDoctor().catch((error) => {
|
|
4
|
+
// JSON consumers must also get structured evidence when a service is
|
|
5
|
+
// unavailable. Do not echo parser excerpts from backend/configuration bodies.
|
|
6
|
+
const reason =
|
|
7
|
+
error instanceof SyntaxError
|
|
8
|
+
? "Telemetry response or configuration is not valid"
|
|
9
|
+
: error instanceof Error
|
|
10
|
+
? error.message
|
|
11
|
+
: "Telemetry verification failed";
|
|
12
|
+
if (process.argv.includes("json")) {
|
|
201
13
|
console.log(
|
|
202
|
-
|
|
14
|
+
JSON.stringify(
|
|
15
|
+
{
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
checkedAt: new Date().toISOString(),
|
|
18
|
+
status: "incomplete",
|
|
19
|
+
completeQuery: false,
|
|
20
|
+
checks: [
|
|
21
|
+
{
|
|
22
|
+
name: "verification",
|
|
23
|
+
status: "unverified",
|
|
24
|
+
evidence: { reason },
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
null,
|
|
29
|
+
2,
|
|
30
|
+
),
|
|
203
31
|
);
|
|
204
|
-
} else if (!localSummary || !localSummaryMicros) {
|
|
205
|
-
console.log(" missing");
|
|
206
|
-
hasProblem = true;
|
|
207
32
|
} else {
|
|
208
|
-
|
|
209
|
-
0,
|
|
210
|
-
(nowMicros - localSummaryMicros) / 1_000_000,
|
|
211
|
-
);
|
|
212
|
-
console.log(` latest: ${localSummary.timestamp}`);
|
|
213
|
-
console.log(` age: ${formatAgeSeconds(ageSeconds)}`);
|
|
214
|
-
console.log(` requestId: ${localSummary.requestId}`);
|
|
215
|
-
console.log(` status: ${localSummary.responseStatus}`);
|
|
216
|
-
console.log(` accountType: ${localSummary.accountType}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (localSummaryMicros) {
|
|
220
|
-
console.log("");
|
|
221
|
-
console.log("Lag vs local summary:");
|
|
222
|
-
for (const stream of STREAMS_TO_CHECK) {
|
|
223
|
-
const item = groupedStreams[stream.type]?.find(
|
|
224
|
-
(entry) => entry.name === stream.name,
|
|
225
|
-
);
|
|
226
|
-
const latestMicros = item?.stats?.doc_time_max ?? null;
|
|
227
|
-
const deltaSeconds = latestMicros
|
|
228
|
-
? Math.abs(localSummaryMicros - latestMicros) / 1_000_000
|
|
229
|
-
: Number.NaN;
|
|
230
|
-
const status = summarizeLag(deltaSeconds, thresholdSeconds);
|
|
231
|
-
if (status !== "fresh") {
|
|
232
|
-
hasProblem = true;
|
|
233
|
-
}
|
|
234
|
-
console.log(
|
|
235
|
-
` ${stream.name}: ${status} (${formatAgeSeconds(deltaSeconds)})`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (hasProblem) {
|
|
241
|
-
process.exitCode = 1;
|
|
33
|
+
console.error(reason);
|
|
242
34
|
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
main().catch((error) => {
|
|
246
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
247
35
|
process.exitCode = 1;
|
|
248
36
|
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Backend query adapter for OTLP telemetry. OTLP itself has no history/query API. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
|
|
7
|
+
/** Discover the existing native collector destination without exposing its credentials. */
|
|
8
|
+
export async function resolveProxyTelemetryBackend(env = process.env) {
|
|
9
|
+
/** @type {import("../../src/lib/types/index.js").ProxyTelemetryNativeConfig | undefined} */
|
|
10
|
+
let native;
|
|
11
|
+
if (!env.NEUROLINK_OPENOBSERVE_URL) {
|
|
12
|
+
try {
|
|
13
|
+
native =
|
|
14
|
+
/** @type {import("../../src/lib/types/index.js").ProxyTelemetryNativeConfig} */ (
|
|
15
|
+
yaml.load(
|
|
16
|
+
await readFile(
|
|
17
|
+
env.NEUROLINK_OTEL_COLLECTOR_CONFIG ??
|
|
18
|
+
join(
|
|
19
|
+
homedir(),
|
|
20
|
+
".neurolink",
|
|
21
|
+
"telemetry-native",
|
|
22
|
+
"config",
|
|
23
|
+
"collector.yaml",
|
|
24
|
+
),
|
|
25
|
+
"utf8",
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (
|
|
31
|
+
!(error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
32
|
+
) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Could not read the configured telemetry collector destination",
|
|
35
|
+
{ cause: error },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const exporter = native?.exporters?.["otlphttp/openobserve"];
|
|
41
|
+
const destination = exporter?.endpoint
|
|
42
|
+
? new URL(exporter.endpoint)
|
|
43
|
+
: undefined;
|
|
44
|
+
const nativeOrg = destination?.pathname.match(
|
|
45
|
+
/^\/api\/([a-zA-Z0-9_-]+)\/?$/,
|
|
46
|
+
)?.[1];
|
|
47
|
+
const authorization =
|
|
48
|
+
env.NEUROLINK_OPENOBSERVE_BASIC_AUTH ??
|
|
49
|
+
(env.NEUROLINK_OPENOBSERVE_USER && env.NEUROLINK_OPENOBSERVE_PASSWORD
|
|
50
|
+
? `Basic ${Buffer.from(`${env.NEUROLINK_OPENOBSERVE_USER}:${env.NEUROLINK_OPENOBSERVE_PASSWORD}`).toString("base64")}`
|
|
51
|
+
: (exporter?.headers?.Authorization ?? exporter?.headers?.authorization));
|
|
52
|
+
return {
|
|
53
|
+
baseUrl:
|
|
54
|
+
env.NEUROLINK_OPENOBSERVE_URL ??
|
|
55
|
+
destination?.origin ??
|
|
56
|
+
"http://127.0.0.1:5080",
|
|
57
|
+
organization: env.NEUROLINK_OPENOBSERVE_ORG ?? nativeOrg ?? "default",
|
|
58
|
+
stream:
|
|
59
|
+
env.NEUROLINK_PROXY_STREAM_HEADER ??
|
|
60
|
+
exporter?.headers?.["stream-name"] ??
|
|
61
|
+
"neurolink_proxy",
|
|
62
|
+
authorization,
|
|
63
|
+
collectorMetricsUrl:
|
|
64
|
+
env.NEUROLINK_OTEL_COLLECTOR_METRICS_URL ??
|
|
65
|
+
(native ? "http://127.0.0.1:14388/metrics" : undefined),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate before issuing any diagnostics request.
|
|
70
|
+
* @param {import("../../src/lib/types/index.js").ProxyTelemetryBackend} backend
|
|
71
|
+
*/
|
|
72
|
+
export function validateProxyTelemetryBackend(backend) {
|
|
73
|
+
if (
|
|
74
|
+
!/^[a-zA-Z0-9_-]+$/.test(backend.organization) ||
|
|
75
|
+
!/^[a-zA-Z0-9_]+$/.test(backend.stream)
|
|
76
|
+
) {
|
|
77
|
+
throw new Error("Invalid telemetry organization or stream");
|
|
78
|
+
}
|
|
79
|
+
const endpoint = new URL(backend.baseUrl);
|
|
80
|
+
if (
|
|
81
|
+
endpoint.username ||
|
|
82
|
+
endpoint.password ||
|
|
83
|
+
!["https:", "http:"].includes(endpoint.protocol)
|
|
84
|
+
) {
|
|
85
|
+
throw new Error("Invalid telemetry endpoint");
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
backend.authorization &&
|
|
89
|
+
endpoint.protocol !== "https:" &&
|
|
90
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Credentialed telemetry queries require HTTPS outside loopback",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (backend.collectorMetricsUrl) {
|
|
97
|
+
const url = new URL(backend.collectorMetricsUrl);
|
|
98
|
+
if (
|
|
99
|
+
url.username ||
|
|
100
|
+
url.password ||
|
|
101
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
|
|
102
|
+
!["http:", "https:"].includes(url.protocol)
|
|
103
|
+
) {
|
|
104
|
+
throw new Error("Collector diagnostics must use loopback HTTP(S)");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A bounded, complete search; no backend response body or authorization enters errors.
|
|
110
|
+
* @param {import("../../src/lib/types/index.js").ProxyTelemetryBackend} backend
|
|
111
|
+
* @param {import("../../src/lib/types/index.js").ProxyTelemetryQueryOptions} options
|
|
112
|
+
* @param {typeof fetch} fetchImpl
|
|
113
|
+
*/
|
|
114
|
+
export async function queryProxyTelemetry(
|
|
115
|
+
backend,
|
|
116
|
+
{ sql, signal = "logs", startTime, endTime, size = 200, offset = 0 },
|
|
117
|
+
fetchImpl = fetch,
|
|
118
|
+
) {
|
|
119
|
+
validateProxyTelemetryBackend(backend);
|
|
120
|
+
if (
|
|
121
|
+
!/^[a-zA-Z0-9_-]+$/.test(backend.organization) ||
|
|
122
|
+
!/^[a-zA-Z0-9_]+$/.test(backend.stream) ||
|
|
123
|
+
!["logs", "traces", "metrics"].includes(signal) ||
|
|
124
|
+
!Number.isSafeInteger(startTime) ||
|
|
125
|
+
!Number.isSafeInteger(endTime) ||
|
|
126
|
+
startTime >= endTime ||
|
|
127
|
+
!Number.isSafeInteger(size) ||
|
|
128
|
+
size < 1 ||
|
|
129
|
+
size > 1000 ||
|
|
130
|
+
!Number.isSafeInteger(offset) ||
|
|
131
|
+
offset < 0
|
|
132
|
+
) {
|
|
133
|
+
throw new Error("Invalid bounded telemetry query");
|
|
134
|
+
}
|
|
135
|
+
const endpoint = new URL(
|
|
136
|
+
`/api/${backend.organization}/_search?type=${signal}`,
|
|
137
|
+
backend.baseUrl,
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
endpoint.username ||
|
|
141
|
+
endpoint.password ||
|
|
142
|
+
!["https:", "http:"].includes(endpoint.protocol)
|
|
143
|
+
) {
|
|
144
|
+
throw new Error("Invalid telemetry endpoint");
|
|
145
|
+
}
|
|
146
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(
|
|
147
|
+
endpoint.hostname,
|
|
148
|
+
);
|
|
149
|
+
if (backend.authorization && endpoint.protocol !== "https:" && !loopback) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Credentialed telemetry queries require HTTPS outside loopback",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const started = globalThis.performance.now();
|
|
155
|
+
const response = await fetchImpl(endpoint, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
redirect: "error",
|
|
158
|
+
signal: globalThis.AbortSignal.timeout(15_000),
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
...(backend.authorization
|
|
162
|
+
? { Authorization: backend.authorization }
|
|
163
|
+
: {}),
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
query: {
|
|
167
|
+
sql,
|
|
168
|
+
start_time: startTime,
|
|
169
|
+
end_time: endTime - 1,
|
|
170
|
+
size,
|
|
171
|
+
from: offset,
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`Telemetry query failed: HTTP ${response.status}`);
|
|
177
|
+
}
|
|
178
|
+
const data = await response.json();
|
|
179
|
+
if (
|
|
180
|
+
data.is_partial === true ||
|
|
181
|
+
(Array.isArray(data.function_error)
|
|
182
|
+
? data.function_error.length
|
|
183
|
+
: data.function_error) ||
|
|
184
|
+
!Array.isArray(data.hits)
|
|
185
|
+
) {
|
|
186
|
+
throw new Error("Telemetry query returned incomplete data");
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
rows: /** @type {Array<Record<string, unknown>>} */ (data.hits),
|
|
190
|
+
query: {
|
|
191
|
+
sql,
|
|
192
|
+
signal,
|
|
193
|
+
startTime,
|
|
194
|
+
endTimeExclusive: endTime,
|
|
195
|
+
size,
|
|
196
|
+
offset,
|
|
197
|
+
rows: data.hits.length,
|
|
198
|
+
complete: true,
|
|
199
|
+
durationMs: Math.round(globalThis.performance.now() - started),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|