@apifuse/provider-sdk 2.2.0-beta.69 → 2.2.0-beta.70

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.
@@ -0,0 +1,522 @@
1
+ import { readdirSync, readFileSync, readlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+
5
+ import { SDK_VERSION } from "./sdk-version.js";
6
+
7
+ /**
8
+ * Internal runtime diagnostics route on the provider's primary listener, next
9
+ * to `/health` and `/readyz`. It answers one JSON report that tells an idle
10
+ * process that still burns CPU apart from a busy one without `exec`: process
11
+ * CPU sampled over a short window, memory, the Node event-loop and
12
+ * active-resource APIs, and on Linux the per-thread CPU and file-descriptor
13
+ * picture from `/proc/self`.
14
+ *
15
+ * Read-only and secret-free. It shares the boundary of `/health` and `/readyz`:
16
+ * the generated provider NetworkPolicy admits only the gateway on this port and
17
+ * the gateway dials canonical operation paths only, so a tenant cannot reach
18
+ * it; operators use `kubectl port-forward`.
19
+ */
20
+ export const RUNTIME_DIAGNOSTICS_ROUTE = "/__apifuse/diagnostics/runtime";
21
+ export const RUNTIME_DIAGNOSTICS_SCHEMA_VERSION = 1 as const;
22
+ /** Query parameter naming the sampling window; clamped, never rejected. */
23
+ export const RUNTIME_DIAGNOSTICS_WINDOW_QUERY = "windowMs";
24
+ export const DEFAULT_RUNTIME_DIAGNOSTICS_WINDOW_MS = 250;
25
+ export const MIN_RUNTIME_DIAGNOSTICS_WINDOW_MS = 50;
26
+ export const MAX_RUNTIME_DIAGNOSTICS_WINDOW_MS = 5_000;
27
+ /**
28
+ * Linux USER_HZ. The unit of `utime`/`stime` in `/proc` stat files is an ABI
29
+ * constant (100 Hz) independent of the kernel's CONFIG_HZ.
30
+ */
31
+ export const PROCFS_CLOCK_TICKS_PER_SECOND = 100;
32
+ export const DEFAULT_PROCFS_SELF_ROOT = "/proc/self";
33
+
34
+ export interface RuntimeDiagnosticsCpuTotals {
35
+ readonly userMicros: number;
36
+ readonly systemMicros: number;
37
+ }
38
+
39
+ export interface RuntimeDiagnosticsCpu {
40
+ /** `process.cpuUsage()` since boot and its average in cores over `uptimeSeconds`. */
41
+ readonly lifetime: RuntimeDiagnosticsCpuTotals & { readonly averageCores: number };
42
+ /** `process.cpuUsage()` delta over the sampling window, as cores (1 = one CPU fully busy). */
43
+ readonly window: RuntimeDiagnosticsCpuTotals & {
44
+ readonly userCores: number;
45
+ readonly systemCores: number;
46
+ readonly totalCores: number;
47
+ };
48
+ }
49
+
50
+ export interface RuntimeDiagnosticsEventLoopUtilization {
51
+ readonly idle: number;
52
+ readonly active: number;
53
+ readonly utilization: number;
54
+ }
55
+
56
+ export interface RuntimeDiagnosticsEventLoop {
57
+ /**
58
+ * false when the runtime returned only zeros for the whole process lifetime.
59
+ * Bun 1.2 and 1.3 stub `performance.eventLoopUtilization`; read
60
+ * `cpu.window` and `procfs.threads` instead.
61
+ */
62
+ readonly reported: boolean;
63
+ readonly lifetime: RuntimeDiagnosticsEventLoopUtilization;
64
+ readonly window: RuntimeDiagnosticsEventLoopUtilization;
65
+ }
66
+
67
+ export interface RuntimeDiagnosticsActiveResources {
68
+ /**
69
+ * false when the runtime listed nothing, although at least the socket that
70
+ * answers this request is alive. Bun 1.2 and 1.3 stub
71
+ * `process.getActiveResourcesInfo`; read `procfs.fileDescriptors` instead.
72
+ */
73
+ readonly reported: boolean;
74
+ readonly total: number;
75
+ readonly byType: Readonly<Record<string, number>>;
76
+ }
77
+
78
+ export interface RuntimeDiagnosticsMemory {
79
+ readonly rss: number;
80
+ readonly heapTotal: number;
81
+ readonly heapUsed: number;
82
+ readonly external: number;
83
+ readonly arrayBuffers: number;
84
+ }
85
+
86
+ export interface RuntimeDiagnosticsThreadCpuTicks {
87
+ readonly userTicks: number;
88
+ readonly systemTicks: number;
89
+ }
90
+
91
+ export interface RuntimeDiagnosticsThreadContextSwitches {
92
+ readonly voluntary: number;
93
+ readonly nonvoluntary: number;
94
+ }
95
+
96
+ export interface RuntimeDiagnosticsThreadWindow extends RuntimeDiagnosticsThreadCpuTicks {
97
+ /** CPU consumed by this thread over the window, in cores. */
98
+ readonly cores: number;
99
+ readonly contextSwitches: RuntimeDiagnosticsThreadContextSwitches;
100
+ }
101
+
102
+ export interface RuntimeDiagnosticsThread {
103
+ readonly tid: number;
104
+ /** Kernel thread name (`comm`, 15 bytes), e.g. `bun`, `HTTP Client`, `HeapHelper`. */
105
+ readonly name: string;
106
+ /** Scheduler state letter at the second sample (`R` running, `S` sleeping, ...). */
107
+ readonly state: string;
108
+ /** Kernel wait channel at the second sample (`0` while running); null when unreadable. */
109
+ readonly waitChannel: string | null;
110
+ readonly lifetime: RuntimeDiagnosticsThreadCpuTicks;
111
+ /** null for a thread that appeared after the first sample. */
112
+ readonly window: RuntimeDiagnosticsThreadWindow | null;
113
+ }
114
+
115
+ export type RuntimeDiagnosticsFdKind =
116
+ | "socket"
117
+ | "eventpoll"
118
+ | "timerfd"
119
+ | "eventfd"
120
+ | "pipe"
121
+ | "file"
122
+ | "other";
123
+
124
+ export interface RuntimeDiagnosticsProcfs {
125
+ readonly clockTicksPerSecond: number;
126
+ /**
127
+ * Elapsed time between the two /proc/self scans. Slightly longer than
128
+ * `window.observedMs` because it encloses the scans themselves; thread
129
+ * `window.cores` is computed over this interval.
130
+ */
131
+ readonly observedMs: number;
132
+ readonly threads: {
133
+ readonly total: number;
134
+ readonly byName: Readonly<Record<string, number>>;
135
+ /** Sorted by window cores descending, so the hottest thread is first. */
136
+ readonly items: readonly RuntimeDiagnosticsThread[];
137
+ };
138
+ readonly fileDescriptors: {
139
+ readonly total: number;
140
+ readonly byKind: Readonly<Record<RuntimeDiagnosticsFdKind, number>>;
141
+ };
142
+ }
143
+
144
+ export interface RuntimeDiagnosticsReport {
145
+ readonly schemaVersion: typeof RUNTIME_DIAGNOSTICS_SCHEMA_VERSION;
146
+ readonly provider: { readonly id: string; readonly version: string };
147
+ readonly sdkVersion: string;
148
+ readonly runtime: {
149
+ readonly bunVersion: string | null;
150
+ readonly nodeCompatVersion: string;
151
+ readonly platform: string;
152
+ readonly arch: string;
153
+ readonly pid: number;
154
+ };
155
+ readonly sampledAt: string;
156
+ readonly uptimeSeconds: number;
157
+ readonly window: { readonly requestedMs: number; readonly observedMs: number };
158
+ readonly cpu: RuntimeDiagnosticsCpu;
159
+ readonly memory: RuntimeDiagnosticsMemory;
160
+ readonly eventLoop: RuntimeDiagnosticsEventLoop;
161
+ readonly activeResources: RuntimeDiagnosticsActiveResources;
162
+ /** null off Linux or when `/proc/self` is unreadable. */
163
+ readonly procfs: RuntimeDiagnosticsProcfs | null;
164
+ }
165
+
166
+ export interface RuntimeDiagnosticsClock {
167
+ now(): number;
168
+ sleep(ms: number): Promise<void>;
169
+ }
170
+
171
+ export interface CollectRuntimeDiagnosticsInput {
172
+ readonly provider: { readonly id: string; readonly version: string };
173
+ /** Sampling window; clamped to [MIN, MAX]. */
174
+ readonly windowMs?: number;
175
+ /** `/proc/self` substitute for tests. */
176
+ readonly procfsRoot?: string;
177
+ readonly clock?: RuntimeDiagnosticsClock;
178
+ }
179
+
180
+ const defaultClock: RuntimeDiagnosticsClock = {
181
+ now: () => performance.now(),
182
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
183
+ };
184
+
185
+ export function normalizeRuntimeDiagnosticsWindowMs(value: number | undefined): number {
186
+ if (value === undefined || !Number.isFinite(value)) return DEFAULT_RUNTIME_DIAGNOSTICS_WINDOW_MS;
187
+ return Math.min(
188
+ MAX_RUNTIME_DIAGNOSTICS_WINDOW_MS,
189
+ Math.max(MIN_RUNTIME_DIAGNOSTICS_WINDOW_MS, Math.trunc(value)),
190
+ );
191
+ }
192
+
193
+ /** Parses the `windowMs` query value; anything unparsable falls back to the default. */
194
+ export function resolveRuntimeDiagnosticsWindowMs(raw: string | undefined | null): number {
195
+ if (raw === undefined || raw === null || raw.trim() === "") {
196
+ return DEFAULT_RUNTIME_DIAGNOSTICS_WINDOW_MS;
197
+ }
198
+ const parsed = Number.parseInt(raw, 10);
199
+ return normalizeRuntimeDiagnosticsWindowMs(Number.isNaN(parsed) ? undefined : parsed);
200
+ }
201
+
202
+ export async function collectRuntimeDiagnostics(
203
+ input: CollectRuntimeDiagnosticsInput,
204
+ ): Promise<RuntimeDiagnosticsReport> {
205
+ const clock = input.clock ?? defaultClock;
206
+ const procfsRoot = input.procfsRoot ?? DEFAULT_PROCFS_SELF_ROOT;
207
+ const requestedMs = normalizeRuntimeDiagnosticsWindowMs(input.windowMs);
208
+
209
+ // Two nested brackets. The procfs scans are synchronous work on the main
210
+ // thread, so the process-level CPU bracket starts after the first scan and
211
+ // closes before the second one: the scans' own CPU never lands inside it.
212
+ // The thread bracket encloses the scans and divides their tick deltas by
213
+ // its own elapsed time, so the scan cost is attributed over the interval it
214
+ // actually spans instead of inflating a short window.
215
+ const threadsStartedAt = clock.now();
216
+ const threadsStart = sampleThreads(procfsRoot);
217
+ const cpuStart = process.cpuUsage();
218
+ const eventLoopStart = sampleEventLoopUtilization();
219
+ const startedAt = clock.now();
220
+ await clock.sleep(requestedMs);
221
+ const observedMs = Math.max(0, clock.now() - startedAt);
222
+ const cpuWindow = process.cpuUsage(cpuStart);
223
+ const eventLoopEnd = sampleEventLoopUtilization();
224
+ const threadsEnd = sampleThreads(procfsRoot);
225
+ const threadsObservedMs = Math.max(0, clock.now() - threadsStartedAt);
226
+ const fileDescriptors = sampleFileDescriptors(procfsRoot);
227
+
228
+ const cpuLifetime = process.cpuUsage();
229
+ const uptimeSeconds = process.uptime();
230
+ const sampledAt = new Date().toISOString();
231
+ const memory = process.memoryUsage();
232
+ const activeResources = sampleActiveResources();
233
+
234
+ return {
235
+ schemaVersion: RUNTIME_DIAGNOSTICS_SCHEMA_VERSION,
236
+ provider: { id: input.provider.id, version: input.provider.version },
237
+ sdkVersion: SDK_VERSION,
238
+ runtime: {
239
+ bunVersion: process.versions.bun ?? null,
240
+ nodeCompatVersion: process.versions.node,
241
+ platform: process.platform,
242
+ arch: process.arch,
243
+ pid: process.pid,
244
+ },
245
+ sampledAt,
246
+ uptimeSeconds,
247
+ window: { requestedMs, observedMs },
248
+ cpu: {
249
+ lifetime: {
250
+ userMicros: cpuLifetime.user,
251
+ systemMicros: cpuLifetime.system,
252
+ averageCores: coresFromMicros(cpuLifetime.user + cpuLifetime.system, uptimeSeconds * 1000),
253
+ },
254
+ window: {
255
+ userMicros: cpuWindow.user,
256
+ systemMicros: cpuWindow.system,
257
+ userCores: coresFromMicros(cpuWindow.user, observedMs),
258
+ systemCores: coresFromMicros(cpuWindow.system, observedMs),
259
+ totalCores: coresFromMicros(cpuWindow.user + cpuWindow.system, observedMs),
260
+ },
261
+ },
262
+ memory: {
263
+ rss: memory.rss,
264
+ heapTotal: memory.heapTotal,
265
+ heapUsed: memory.heapUsed,
266
+ external: memory.external,
267
+ arrayBuffers: memory.arrayBuffers,
268
+ },
269
+ eventLoop: {
270
+ reported: eventLoopEnd.idle + eventLoopEnd.active > 0,
271
+ lifetime: eventLoopEnd,
272
+ window: eventLoopUtilizationDelta(eventLoopEnd, eventLoopStart),
273
+ },
274
+ activeResources,
275
+ procfs:
276
+ threadsEnd === null || fileDescriptors === null
277
+ ? null
278
+ : {
279
+ clockTicksPerSecond: PROCFS_CLOCK_TICKS_PER_SECOND,
280
+ observedMs: threadsObservedMs,
281
+ threads: summarizeThreads(threadsStart, threadsEnd, threadsObservedMs),
282
+ fileDescriptors,
283
+ },
284
+ };
285
+ }
286
+
287
+ function coresFromMicros(micros: number, elapsedMs: number): number {
288
+ if (!(elapsedMs > 0)) return 0;
289
+ return micros / 1000 / elapsedMs;
290
+ }
291
+
292
+ function sampleEventLoopUtilization(): RuntimeDiagnosticsEventLoopUtilization {
293
+ const eventLoopUtilization: unknown = performance.eventLoopUtilization;
294
+ if (typeof eventLoopUtilization !== "function") return { idle: 0, active: 0, utilization: 0 };
295
+ try {
296
+ const sample: unknown = (eventLoopUtilization as () => unknown).call(performance);
297
+ return {
298
+ idle: numberProperty(sample, "idle"),
299
+ active: numberProperty(sample, "active"),
300
+ utilization: numberProperty(sample, "utilization"),
301
+ };
302
+ } catch {
303
+ return { idle: 0, active: 0, utilization: 0 };
304
+ }
305
+ }
306
+
307
+ function eventLoopUtilizationDelta(
308
+ end: RuntimeDiagnosticsEventLoopUtilization,
309
+ start: RuntimeDiagnosticsEventLoopUtilization,
310
+ ): RuntimeDiagnosticsEventLoopUtilization {
311
+ const idle = Math.max(0, end.idle - start.idle);
312
+ const active = Math.max(0, end.active - start.active);
313
+ const total = idle + active;
314
+ return { idle, active, utilization: total > 0 ? active / total : 0 };
315
+ }
316
+
317
+ function sampleActiveResources(): RuntimeDiagnosticsActiveResources {
318
+ const getActiveResourcesInfo: unknown = process.getActiveResourcesInfo;
319
+ let types: string[] = [];
320
+ if (typeof getActiveResourcesInfo === "function") {
321
+ try {
322
+ const listed: unknown = (getActiveResourcesInfo as () => unknown).call(process);
323
+ if (Array.isArray(listed)) {
324
+ types = listed.filter((entry): entry is string => typeof entry === "string");
325
+ }
326
+ } catch {
327
+ types = [];
328
+ }
329
+ }
330
+ const byType: Record<string, number> = {};
331
+ for (const type of types) byType[type] = (byType[type] ?? 0) + 1;
332
+ return { reported: types.length > 0, total: types.length, byType };
333
+ }
334
+
335
+ function numberProperty(value: unknown, key: string): number {
336
+ if (value === null || typeof value !== "object") return 0;
337
+ const candidate: unknown = (value as Record<string, unknown>)[key];
338
+ return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : 0;
339
+ }
340
+
341
+ interface ThreadSample {
342
+ readonly tid: number;
343
+ readonly name: string;
344
+ readonly state: string;
345
+ readonly userTicks: number;
346
+ readonly systemTicks: number;
347
+ readonly voluntary: number;
348
+ readonly nonvoluntary: number;
349
+ readonly waitChannel: string | null;
350
+ }
351
+
352
+ function sampleThreads(root: string): Map<number, ThreadSample> | null {
353
+ let entries: string[];
354
+ try {
355
+ entries = readdirSync(join(root, "task"));
356
+ } catch {
357
+ return null;
358
+ }
359
+ const samples = new Map<number, ThreadSample>();
360
+ for (const entry of entries) {
361
+ if (!/^\d+$/.test(entry)) continue;
362
+ const sample = sampleThread(join(root, "task", entry), Number.parseInt(entry, 10));
363
+ if (sample) samples.set(sample.tid, sample);
364
+ }
365
+ return samples;
366
+ }
367
+
368
+ function sampleThread(taskDir: string, tid: number): ThreadSample | null {
369
+ let stat: string;
370
+ try {
371
+ stat = readFileSync(join(taskDir, "stat"), "utf8");
372
+ } catch {
373
+ // The thread exited between the directory listing and this read.
374
+ return null;
375
+ }
376
+ const open = stat.indexOf("(");
377
+ const close = stat.lastIndexOf(")");
378
+ if (open < 0 || close < open) return null;
379
+ const fields = stat
380
+ .slice(close + 2)
381
+ .trimEnd()
382
+ .split(" ");
383
+ const userTicks = Number.parseInt(fields[11] ?? "", 10);
384
+ const systemTicks = Number.parseInt(fields[12] ?? "", 10);
385
+ if (!Number.isFinite(userTicks) || !Number.isFinite(systemTicks)) return null;
386
+ const switches = readContextSwitches(join(taskDir, "status"));
387
+ return {
388
+ tid,
389
+ name: stat.slice(open + 1, close),
390
+ state: fields[0] ?? "?",
391
+ userTicks,
392
+ systemTicks,
393
+ voluntary: switches.voluntary,
394
+ nonvoluntary: switches.nonvoluntary,
395
+ waitChannel: readWaitChannel(join(taskDir, "wchan")),
396
+ };
397
+ }
398
+
399
+ function readContextSwitches(statusPath: string): RuntimeDiagnosticsThreadContextSwitches {
400
+ let voluntary = 0;
401
+ let nonvoluntary = 0;
402
+ let status: string;
403
+ try {
404
+ status = readFileSync(statusPath, "utf8");
405
+ } catch {
406
+ return { voluntary, nonvoluntary };
407
+ }
408
+ for (const line of status.split("\n")) {
409
+ const separator = line.indexOf(":");
410
+ if (separator < 0) continue;
411
+ const key = line.slice(0, separator);
412
+ if (key !== "voluntary_ctxt_switches" && key !== "nonvoluntary_ctxt_switches") continue;
413
+ const parsed = Number.parseInt(line.slice(separator + 1).trim(), 10);
414
+ if (!Number.isFinite(parsed)) continue;
415
+ if (key === "voluntary_ctxt_switches") voluntary = parsed;
416
+ else nonvoluntary = parsed;
417
+ }
418
+ return { voluntary, nonvoluntary };
419
+ }
420
+
421
+ function readWaitChannel(wchanPath: string): string | null {
422
+ try {
423
+ const value = readFileSync(wchanPath, "utf8").trim();
424
+ return value === "" ? null : value;
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+
430
+ function summarizeThreads(
431
+ start: Map<number, ThreadSample> | null,
432
+ end: Map<number, ThreadSample>,
433
+ observedMs: number,
434
+ ): RuntimeDiagnosticsProcfs["threads"] {
435
+ const byName: Record<string, number> = {};
436
+ const items: RuntimeDiagnosticsThread[] = [];
437
+ for (const sample of end.values()) {
438
+ byName[sample.name] = (byName[sample.name] ?? 0) + 1;
439
+ const baseline = start?.get(sample.tid);
440
+ let window: RuntimeDiagnosticsThreadWindow | null = null;
441
+ if (baseline) {
442
+ const userTicks = Math.max(0, sample.userTicks - baseline.userTicks);
443
+ const systemTicks = Math.max(0, sample.systemTicks - baseline.systemTicks);
444
+ window = {
445
+ userTicks,
446
+ systemTicks,
447
+ cores: coresFromTicks(userTicks + systemTicks, observedMs),
448
+ contextSwitches: {
449
+ voluntary: Math.max(0, sample.voluntary - baseline.voluntary),
450
+ nonvoluntary: Math.max(0, sample.nonvoluntary - baseline.nonvoluntary),
451
+ },
452
+ };
453
+ }
454
+ items.push({
455
+ tid: sample.tid,
456
+ name: sample.name,
457
+ state: sample.state,
458
+ waitChannel: sample.waitChannel,
459
+ lifetime: { userTicks: sample.userTicks, systemTicks: sample.systemTicks },
460
+ window,
461
+ });
462
+ }
463
+ items.sort(compareThreads);
464
+ return { total: items.length, byName, items };
465
+ }
466
+
467
+ function coresFromTicks(ticks: number, elapsedMs: number): number {
468
+ if (!(elapsedMs > 0)) return 0;
469
+ return ((ticks / PROCFS_CLOCK_TICKS_PER_SECOND) * 1000) / elapsedMs;
470
+ }
471
+
472
+ function compareThreads(left: RuntimeDiagnosticsThread, right: RuntimeDiagnosticsThread): number {
473
+ const leftCores = left.window?.cores ?? -1;
474
+ const rightCores = right.window?.cores ?? -1;
475
+ if (leftCores !== rightCores) return rightCores - leftCores;
476
+ const leftLifetime = left.lifetime.userTicks + left.lifetime.systemTicks;
477
+ const rightLifetime = right.lifetime.userTicks + right.lifetime.systemTicks;
478
+ if (leftLifetime !== rightLifetime) return rightLifetime - leftLifetime;
479
+ return left.tid - right.tid;
480
+ }
481
+
482
+ function sampleFileDescriptors(root: string): RuntimeDiagnosticsProcfs["fileDescriptors"] | null {
483
+ let entries: string[];
484
+ try {
485
+ entries = readdirSync(join(root, "fd"));
486
+ } catch {
487
+ return null;
488
+ }
489
+ const byKind: Record<RuntimeDiagnosticsFdKind, number> = {
490
+ socket: 0,
491
+ eventpoll: 0,
492
+ timerfd: 0,
493
+ eventfd: 0,
494
+ pipe: 0,
495
+ file: 0,
496
+ other: 0,
497
+ };
498
+ let total = 0;
499
+ for (const entry of entries) {
500
+ let target: string;
501
+ try {
502
+ target = readlinkSync(join(root, "fd", entry));
503
+ } catch {
504
+ // Closed between the listing and the readlink (the listing's own
505
+ // directory handle always lands here).
506
+ continue;
507
+ }
508
+ byKind[classifyFileDescriptor(target)] += 1;
509
+ total += 1;
510
+ }
511
+ return { total, byKind };
512
+ }
513
+
514
+ function classifyFileDescriptor(target: string): RuntimeDiagnosticsFdKind {
515
+ if (target.startsWith("socket:")) return "socket";
516
+ if (target === "anon_inode:[eventpoll]") return "eventpoll";
517
+ if (target === "anon_inode:[timerfd]") return "timerfd";
518
+ if (target === "anon_inode:[eventfd]") return "eventfd";
519
+ if (target.startsWith("pipe:")) return "pipe";
520
+ if (target.startsWith("/")) return "file";
521
+ return "other";
522
+ }
@@ -222,6 +222,12 @@ import {
222
222
  type ProviderStateBackendLogEvent,
223
223
  providerStateBackendLogEvent,
224
224
  } from "./state-backend-report.js";
225
+ import {
226
+ collectRuntimeDiagnostics,
227
+ resolveRuntimeDiagnosticsWindowMs,
228
+ RUNTIME_DIAGNOSTICS_ROUTE,
229
+ RUNTIME_DIAGNOSTICS_WINDOW_QUERY,
230
+ } from "./runtime-diagnostics.js";
225
231
  import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
226
232
  import {
227
233
  collectStaticDiagnosticSensitiveValues,
@@ -3821,6 +3827,22 @@ function createServerAppWithCapabilityModules(
3821
3827
  ),
3822
3828
  );
3823
3829
 
3830
+ // Internal runtime diagnostics: process CPU over a short window, memory,
3831
+ // the Node event-loop/active-resource APIs and, on Linux, per-thread CPU and
3832
+ // file-descriptor counts from /proc/self. Tells an idle pod that still burns
3833
+ // CPU apart from a busy one without exec. Read-only and secret-free, behind
3834
+ // the same boundary as /health and /readyz: the provider NetworkPolicy only
3835
+ // admits the gateway on this port and the gateway dials canonical operation
3836
+ // paths only, so operators reach it through `kubectl port-forward`.
3837
+ app.get(RUNTIME_DIAGNOSTICS_ROUTE, async (c) => {
3838
+ const report = await collectRuntimeDiagnostics({
3839
+ provider: { id: provider.id, version: provider.version },
3840
+ windowMs: resolveRuntimeDiagnosticsWindowMs(c.req.query(RUNTIME_DIAGNOSTICS_WINDOW_QUERY)),
3841
+ });
3842
+ c.header("Cache-Control", "no-store");
3843
+ return c.json(report);
3844
+ });
3845
+
3824
3846
  app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
3825
3847
  let rawBodyText = "";
3826
3848
  let rawBody: unknown;