@gtadi/k8s-node-debugger 1.0.0
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/LICENSE +183 -0
- package/README.md +118 -0
- package/bin/k8s-node-debugger.js +168 -0
- package/package.json +28 -0
- package/public/app.js +547 -0
- package/public/conntrack-view.js +569 -0
- package/public/health-view.js +462 -0
- package/public/index.html +90 -0
- package/public/iptables-view.js +523 -0
- package/public/style.css +866 -0
- package/src/k8s.js +196 -0
- package/src/probes.js +255 -0
- package/src/server.js +187 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
* k8s-node-debugger — Node Health renderers
|
|
4
|
+
* Exposes: renderMemInfoView, renderMemPressureView, renderOomKillsView,
|
|
5
|
+
* renderKubeletLogsView, renderDiskView, renderCpuView
|
|
6
|
+
* ══════════════════════════════════════════════════════════════════════════ */
|
|
7
|
+
(function () {
|
|
8
|
+
|
|
9
|
+
function h(s) {
|
|
10
|
+
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hBytes(kb) {
|
|
14
|
+
if (!kb || kb < 1) return '0 B';
|
|
15
|
+
const mb = kb / 1024, gb = mb / 1024;
|
|
16
|
+
if (gb >= 1) return gb.toFixed(2) + ' GiB';
|
|
17
|
+
if (mb >= 1) return mb.toFixed(1) + ' MiB';
|
|
18
|
+
return kb + ' KiB';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pct(used, total) {
|
|
22
|
+
return total ? Math.min(Math.round(used / total * 100), 100) : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function colorCls(p) {
|
|
26
|
+
return p >= 90 ? 'hv-crit' : p >= 70 ? 'hv-warn' : 'hv-ok';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function gauge(used, total, label, subText) {
|
|
30
|
+
const p = pct(used, total);
|
|
31
|
+
const cls = colorCls(p);
|
|
32
|
+
return `
|
|
33
|
+
<div class="hv-gauge-wrap">
|
|
34
|
+
<div class="hv-gauge-hdr">
|
|
35
|
+
<span class="hv-gauge-title">${h(label)}</span>
|
|
36
|
+
<span class="hv-gauge-pct-lbl ${cls}">${p}%</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="hv-gauge-bar"><div class="hv-gauge-fill ${cls}" style="width:${p}%"></div></div>
|
|
39
|
+
<div class="hv-gauge-labels"><span>${h(subText)}</span></div>
|
|
40
|
+
</div>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
44
|
+
* Memory — /proc/meminfo
|
|
45
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
46
|
+
function renderMemInfoView(raw, container) {
|
|
47
|
+
const kv = {};
|
|
48
|
+
for (const line of raw.split('\n')) {
|
|
49
|
+
const m = line.match(/^(\w+):\s+(\d+)/);
|
|
50
|
+
if (m) kv[m[1]] = +m[2];
|
|
51
|
+
}
|
|
52
|
+
if (!Object.keys(kv).length) { container.textContent = raw; return; }
|
|
53
|
+
|
|
54
|
+
const total = kv.MemTotal || 0;
|
|
55
|
+
const avail = kv.MemAvailable || 0;
|
|
56
|
+
const free = kv.MemFree || 0;
|
|
57
|
+
const buffers = kv.Buffers || 0;
|
|
58
|
+
const cached = Math.max((kv.Cached || 0) + (kv.SReclaimable || 0) - (kv.Shmem || 0), 0);
|
|
59
|
+
const used = total - avail;
|
|
60
|
+
const swapTotal = kv.SwapTotal || 0;
|
|
61
|
+
const swapFree = kv.SwapFree || 0;
|
|
62
|
+
const swapUsed = swapTotal - swapFree;
|
|
63
|
+
|
|
64
|
+
const wrap = document.createElement('div');
|
|
65
|
+
wrap.className = 'hv-wrap';
|
|
66
|
+
|
|
67
|
+
wrap.innerHTML = `
|
|
68
|
+
<div class="hv-gauges">
|
|
69
|
+
${gauge(used, total, 'RAM used',
|
|
70
|
+
`${hBytes(used)} used · ${hBytes(avail)} available · ${hBytes(total)} total`)}
|
|
71
|
+
${swapTotal > 0 ? gauge(swapUsed, swapTotal, 'Swap used',
|
|
72
|
+
`${hBytes(swapUsed)} used · ${hBytes(swapFree)} free · ${hBytes(swapTotal)} total`) : ''}
|
|
73
|
+
</div>
|
|
74
|
+
<div class="hv-grid">
|
|
75
|
+
${[
|
|
76
|
+
['Total RAM', hBytes(total)],
|
|
77
|
+
['Used', hBytes(used)],
|
|
78
|
+
['Available', hBytes(avail)],
|
|
79
|
+
['Free', hBytes(free)],
|
|
80
|
+
['Buffers', hBytes(buffers)],
|
|
81
|
+
['Page cache', hBytes(cached)],
|
|
82
|
+
['Swap total', swapTotal ? hBytes(swapTotal) : '—'],
|
|
83
|
+
['Swap used', swapTotal ? hBytes(swapUsed) : '—'],
|
|
84
|
+
['Hugepages', kv.HugePages_Total
|
|
85
|
+
? `${kv.HugePages_Total} × ${hBytes(kv.Hugepagesize || 0)}`
|
|
86
|
+
: '—'],
|
|
87
|
+
].map(([k, v]) => `
|
|
88
|
+
<div class="hv-grid-item">
|
|
89
|
+
<div class="hv-grid-label">${h(k)}</div>
|
|
90
|
+
<div class="hv-grid-val">${h(v)}</div>
|
|
91
|
+
</div>`).join('')}
|
|
92
|
+
</div>`;
|
|
93
|
+
|
|
94
|
+
container.innerHTML = '';
|
|
95
|
+
container.className = '';
|
|
96
|
+
container.appendChild(wrap);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
100
|
+
* PSI pressure — /proc/pressure/{cpu,memory,io}
|
|
101
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
102
|
+
function renderMemPressureView(raw, container) {
|
|
103
|
+
const sections = {};
|
|
104
|
+
let cur = null;
|
|
105
|
+
for (const line of raw.split('\n')) {
|
|
106
|
+
const sec = line.match(/^=(\w+)=$/);
|
|
107
|
+
if (sec) { cur = sec[1]; sections[cur] = []; continue; }
|
|
108
|
+
if (cur && line.trim()) sections[cur].push(line.trim());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parsePSI(lines) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const type = line.match(/^(some|full)/)?.[1];
|
|
115
|
+
if (!type) continue;
|
|
116
|
+
out[type] = {
|
|
117
|
+
avg10: parseFloat(line.match(/avg10=([0-9.]+)/)?.[1] || '0'),
|
|
118
|
+
avg60: parseFloat(line.match(/avg60=([0-9.]+)/)?.[1] || '0'),
|
|
119
|
+
avg300: parseFloat(line.match(/avg300=([0-9.]+)/)?.[1] || '0'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function psiValColor(v) {
|
|
126
|
+
return v >= 20 ? 'hv-crit' : v >= 5 ? 'hv-warn' : 'hv-ok';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderBox(name, lines) {
|
|
130
|
+
if (!lines || !lines.length || lines[0] === 'n/a') {
|
|
131
|
+
return `
|
|
132
|
+
<div class="hv-psi-box">
|
|
133
|
+
<div class="hv-psi-title">${h(name.toUpperCase())}</div>
|
|
134
|
+
<div class="hv-psi-na">Not available on this kernel</div>
|
|
135
|
+
</div>`;
|
|
136
|
+
}
|
|
137
|
+
const data = parsePSI(lines);
|
|
138
|
+
const maxAvg10 = Math.max(...Object.values(data).map(d => d.avg10), 0);
|
|
139
|
+
const boxCls = maxAvg10 >= 20 ? 'hv-psi-box-crit' : maxAvg10 >= 5 ? 'hv-psi-box-warn' : 'hv-psi-box-ok';
|
|
140
|
+
|
|
141
|
+
const rows = Object.entries(data).map(([type, d]) => `
|
|
142
|
+
<div class="hv-psi-row">
|
|
143
|
+
<span class="hv-psi-key">${h(type)}</span>
|
|
144
|
+
<span class="hv-psi-val ${psiValColor(d.avg10)}" title="10-second window">avg10 = ${d.avg10.toFixed(2)}%</span>
|
|
145
|
+
<span class="hv-psi-val ${psiValColor(d.avg60)}" title="60-second window">avg60 = ${d.avg60.toFixed(2)}%</span>
|
|
146
|
+
<span class="hv-psi-val ${psiValColor(d.avg300)}" title="300-second window">avg300 = ${d.avg300.toFixed(2)}%</span>
|
|
147
|
+
</div>`).join('');
|
|
148
|
+
|
|
149
|
+
return `
|
|
150
|
+
<div class="hv-psi-box ${boxCls}">
|
|
151
|
+
<div class="hv-psi-title">${h(name.toUpperCase())}</div>
|
|
152
|
+
<div class="hv-psi-rows">${rows}</div>
|
|
153
|
+
</div>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const wrap = document.createElement('div');
|
|
157
|
+
wrap.className = 'hv-wrap';
|
|
158
|
+
wrap.innerHTML = `
|
|
159
|
+
<div class="hv-info-note">
|
|
160
|
+
<b>PSI (Pressure Stall Information)</b> — % of time tasks were stalled waiting for this resource.
|
|
161
|
+
avg10 ≥ 5% = moderate contention · ≥ 20% = severe.
|
|
162
|
+
<em>some</em> = at least one task stalled; <em>full</em> = all tasks stalled (CPU only has <em>some</em>).
|
|
163
|
+
</div>
|
|
164
|
+
<div class="hv-psi-boxes">
|
|
165
|
+
${renderBox('cpu', sections.cpu)}
|
|
166
|
+
${renderBox('memory', sections.memory)}
|
|
167
|
+
${renderBox('io', sections.io)}
|
|
168
|
+
</div>`;
|
|
169
|
+
|
|
170
|
+
container.innerHTML = '';
|
|
171
|
+
container.className = '';
|
|
172
|
+
container.appendChild(wrap);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
176
|
+
* OOM kills — dmesg
|
|
177
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
178
|
+
function renderOomKillsView(raw, container) {
|
|
179
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
180
|
+
container.innerHTML = '';
|
|
181
|
+
container.className = '';
|
|
182
|
+
|
|
183
|
+
if (!lines.length) {
|
|
184
|
+
container.innerHTML = '<div class="hv-oom-ok">✔ No OOM kill events found since last boot.</div>';
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Group into events: flush when we hit a "Killed process" line
|
|
189
|
+
const events = [];
|
|
190
|
+
let cur = [];
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
cur.push(line);
|
|
193
|
+
if (/killed process|oom_kill_process/i.test(line)) {
|
|
194
|
+
events.push(cur);
|
|
195
|
+
cur = [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (cur.length) events.push(cur);
|
|
199
|
+
|
|
200
|
+
function parseEvent(evLines) {
|
|
201
|
+
const full = evLines.join('\n');
|
|
202
|
+
return {
|
|
203
|
+
proc: full.match(/Killed process \d+ \(([^)]+)\)/i)?.[1]
|
|
204
|
+
|| full.match(/oom_kill_process.*?"([^"]+)"/)?.[1] || null,
|
|
205
|
+
pid: full.match(/Killed process (\d+)/i)?.[1] || null,
|
|
206
|
+
reqKb: +(full.match(/anon-rss:(\d+)kB/i)?.[1] || full.match(/rss:(\d+)kB/i)?.[1] || 0),
|
|
207
|
+
totalKb: +(full.match(/total-vm:(\d+)kB/i)?.[1] || 0),
|
|
208
|
+
ts: evLines[0].match(/^\[?([^\]]+)\]?/)?.[1]?.trim() || '',
|
|
209
|
+
raw: full,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const wrap = document.createElement('div');
|
|
214
|
+
wrap.className = 'hv-wrap';
|
|
215
|
+
wrap.innerHTML = `<div class="hv-oom-count-badge">⚠ ${events.length} OOM kill event${events.length !== 1 ? 's' : ''} found since last boot</div>`;
|
|
216
|
+
|
|
217
|
+
const list = document.createElement('div');
|
|
218
|
+
list.className = 'hv-oom-list';
|
|
219
|
+
|
|
220
|
+
for (const evLines of [...events].reverse()) {
|
|
221
|
+
const ev = parseEvent(evLines);
|
|
222
|
+
const card = document.createElement('div');
|
|
223
|
+
card.className = 'hv-oom-event';
|
|
224
|
+
card.innerHTML = `
|
|
225
|
+
<div class="hv-oom-header">
|
|
226
|
+
<span class="hv-oom-process">${h(ev.proc || 'unknown process')}</span>
|
|
227
|
+
${ev.pid ? `<span class="hv-oom-pid">PID ${h(ev.pid)}</span>` : ''}
|
|
228
|
+
${ev.ts ? `<span class="hv-oom-ts">${h(ev.ts)}</span>` : ''}
|
|
229
|
+
</div>
|
|
230
|
+
${ev.reqKb || ev.totalKb ? `
|
|
231
|
+
<div class="hv-oom-mems">
|
|
232
|
+
${ev.reqKb ? `<div class="hv-oom-mem-item"><span class="hv-oom-mem-label">RSS </span><span class="hv-oom-mem-val">${hBytes(ev.reqKb)}</span></div>` : ''}
|
|
233
|
+
${ev.totalKb ? `<div class="hv-oom-mem-item"><span class="hv-oom-mem-label">Total-VM </span><span class="hv-oom-mem-val">${hBytes(ev.totalKb)}</span></div>` : ''}
|
|
234
|
+
</div>` : ''}
|
|
235
|
+
<pre class="hv-oom-raw">${h(ev.raw)}</pre>`;
|
|
236
|
+
list.appendChild(card);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
wrap.appendChild(list);
|
|
240
|
+
container.appendChild(wrap);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
244
|
+
* Kubelet logs — journalctl
|
|
245
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
246
|
+
function renderKubeletLogsView(raw, container) {
|
|
247
|
+
const EVICT_RE = /evict|threshold|pressure|diskpressure|memorypressure|nodecondition|notready|imagegarbage/i;
|
|
248
|
+
|
|
249
|
+
function lineLevel(line) {
|
|
250
|
+
const l = line.toLowerCase();
|
|
251
|
+
if (/\berror\b|\bfatal\b|\bpanic\b/.test(l)) return 'error';
|
|
252
|
+
if (/\bwarn/.test(l)) return 'warn';
|
|
253
|
+
if (EVICT_RE.test(l)) return 'evict';
|
|
254
|
+
return 'normal';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const counts = { error: 0, warn: 0, evict: 0 };
|
|
258
|
+
const classified = raw.split('\n').map(line => {
|
|
259
|
+
const level = lineLevel(line);
|
|
260
|
+
if (level !== 'normal') counts[level]++;
|
|
261
|
+
return { line, level };
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const wrap = document.createElement('div');
|
|
265
|
+
wrap.className = 'hv-wrap';
|
|
266
|
+
|
|
267
|
+
const pills = [
|
|
268
|
+
counts.error > 0
|
|
269
|
+
? `<span class="hv-log-pill hv-log-pill-error">${counts.error} error${counts.error !== 1 ? 's' : ''}</span>`
|
|
270
|
+
: `<span class="hv-log-pill hv-log-pill-ok">0 errors</span>`,
|
|
271
|
+
counts.warn > 0 ? `<span class="hv-log-pill hv-log-pill-warn">${counts.warn} warning${counts.warn !== 1 ? 's' : ''}</span>` : '',
|
|
272
|
+
counts.evict > 0 ? `<span class="hv-log-pill hv-log-pill-evict">${counts.evict} eviction/pressure event${counts.evict !== 1 ? 's' : ''}</span>` : '',
|
|
273
|
+
].filter(Boolean).join('');
|
|
274
|
+
|
|
275
|
+
const summaryDiv = document.createElement('div');
|
|
276
|
+
summaryDiv.className = 'hv-log-summary';
|
|
277
|
+
summaryDiv.innerHTML = pills;
|
|
278
|
+
wrap.appendChild(summaryDiv);
|
|
279
|
+
|
|
280
|
+
const logDiv = document.createElement('div');
|
|
281
|
+
logDiv.className = 'hv-log-lines';
|
|
282
|
+
logDiv.innerHTML = classified.map(({ line, level }) =>
|
|
283
|
+
`<div class="hv-log-line hv-log-${level}">${h(line)}</div>`
|
|
284
|
+
).join('');
|
|
285
|
+
wrap.appendChild(logDiv);
|
|
286
|
+
|
|
287
|
+
container.innerHTML = '';
|
|
288
|
+
container.className = '';
|
|
289
|
+
container.appendChild(wrap);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
293
|
+
* Disk usage — df -h
|
|
294
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
295
|
+
function renderDiskView(raw, container) {
|
|
296
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
297
|
+
if (lines.length < 2) { container.textContent = raw; return; }
|
|
298
|
+
|
|
299
|
+
const SKIP = /^(tmpfs|devtmpfs|udev|none)\s/;
|
|
300
|
+
const CRITICAL = new Set(['/var/lib/kubelet', '/var/lib/containerd', '/var/lib/docker', '/']);
|
|
301
|
+
|
|
302
|
+
const rows = [];
|
|
303
|
+
for (const line of lines.slice(1)) {
|
|
304
|
+
const parts = line.split(/\s+/);
|
|
305
|
+
if (parts.length < 6) continue;
|
|
306
|
+
const [fs, size, used, avail, usePct, ...rest] = parts;
|
|
307
|
+
const mount = rest.join(' ');
|
|
308
|
+
const p = parseInt(usePct);
|
|
309
|
+
if (SKIP.test(line) && p < 50) continue;
|
|
310
|
+
rows.push({ fs, size, used, avail, pct: p, mount });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!rows.length) { container.textContent = raw; return; }
|
|
314
|
+
|
|
315
|
+
const wrap = document.createElement('div');
|
|
316
|
+
wrap.className = 'hv-wrap';
|
|
317
|
+
|
|
318
|
+
const rowsHtml = rows.map(r => {
|
|
319
|
+
const cls = colorCls(r.pct);
|
|
320
|
+
const critical = CRITICAL.has(r.mount);
|
|
321
|
+
return `
|
|
322
|
+
<div class="hv-disk-row">
|
|
323
|
+
<div class="hv-disk-header">
|
|
324
|
+
<span class="hv-disk-mount">${h(r.mount)}${critical ? ' <span class="hv-disk-star">kubelet</span>' : ''}</span>
|
|
325
|
+
<span class="hv-disk-fs">${h(r.fs)}</span>
|
|
326
|
+
<div class="hv-disk-sizes">
|
|
327
|
+
<span class="hv-disk-pct ${cls}">${r.pct}%</span>
|
|
328
|
+
<span class="hv-disk-nums">${h(r.used)} used · ${h(r.avail)} free · ${h(r.size)} total</span>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="hv-disk-bar"><div class="hv-disk-fill ${cls}" style="width:${r.pct}%"></div></div>
|
|
332
|
+
</div>`;
|
|
333
|
+
}).join('');
|
|
334
|
+
|
|
335
|
+
wrap.innerHTML = `
|
|
336
|
+
<div class="hv-info-note">
|
|
337
|
+
Kubelet triggers <b>disk-pressure eviction</b> when <code>/</code>, <code>/var/lib/kubelet</code>,
|
|
338
|
+
or <code>/var/lib/containerd</code> exceeds the eviction threshold (default 85%).
|
|
339
|
+
Paths marked <b>kubelet</b> are watched.
|
|
340
|
+
</div>
|
|
341
|
+
<div class="hv-disk-list">${rowsHtml}</div>`;
|
|
342
|
+
|
|
343
|
+
container.innerHTML = '';
|
|
344
|
+
container.className = '';
|
|
345
|
+
container.appendChild(wrap);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
349
|
+
* CPU & load average
|
|
350
|
+
* ══════════════════════════════════════════════════════════════════════ */
|
|
351
|
+
function renderCpuView(raw, container) {
|
|
352
|
+
const sections = {};
|
|
353
|
+
let cur = null;
|
|
354
|
+
for (const line of raw.split('\n')) {
|
|
355
|
+
const sec = line.match(/^=(\w+)=$/);
|
|
356
|
+
if (sec) { cur = sec[1]; sections[cur] = []; continue; }
|
|
357
|
+
if (cur) sections[cur].push(line);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const loadavgLine = (sections.loadavg || []).find(l => l.trim());
|
|
361
|
+
const nprocLine = (sections.nproc || []).find(l => l.trim());
|
|
362
|
+
const modelLine = (sections.cpumodel || []).find(l => l.trim());
|
|
363
|
+
const statLine = (sections.procstat || []).find(l => l.startsWith('cpu '));
|
|
364
|
+
// ps aux: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
|
|
365
|
+
const procLines = (sections.topproc || []).filter(l => l.trim()).slice(1);
|
|
366
|
+
|
|
367
|
+
const nproc = nprocLine ? parseInt(nprocLine) : 1;
|
|
368
|
+
let load1 = 0, load5 = 0, load15 = 0;
|
|
369
|
+
if (loadavgLine) [load1, load5, load15] = loadavgLine.trim().split(' ').slice(0, 3).map(Number);
|
|
370
|
+
|
|
371
|
+
function loadColor(v) {
|
|
372
|
+
const r = v / nproc;
|
|
373
|
+
return r >= 2 ? 'hv-crit' : r >= 1 ? 'hv-warn' : 'hv-ok';
|
|
374
|
+
}
|
|
375
|
+
function loadCardCls(v) {
|
|
376
|
+
const r = v / nproc;
|
|
377
|
+
return r >= 2 ? 'hv-load-card-crit' : r >= 1 ? 'hv-load-card-warn' : 'hv-load-card-ok';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let stealPct = null;
|
|
381
|
+
if (statLine) {
|
|
382
|
+
const [, user, nice, system, idle, iowait, irq, softirq, steal = 0] = statLine.split(/\s+/).map(Number);
|
|
383
|
+
const total = user + nice + system + idle + iowait + irq + softirq + steal;
|
|
384
|
+
if (total > 0) stealPct = (steal / total * 100).toFixed(2);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const loadRatio = load1 / nproc;
|
|
388
|
+
const loadStatus = loadRatio >= 2
|
|
389
|
+
? `⚠ Critically high — load is ${loadRatio.toFixed(1)}× core count. Processes are queuing for CPU.`
|
|
390
|
+
: loadRatio >= 1
|
|
391
|
+
? `↗ Above core count — some CPU queueing (${loadRatio.toFixed(1)}× cores). Worth investigating.`
|
|
392
|
+
: `✔ Normal — load is ${(loadRatio * 100).toFixed(0)}% of core capacity.`;
|
|
393
|
+
const loadNoteCls = loadRatio >= 2 ? 'hv-note-crit' : loadRatio >= 1 ? 'hv-note-warn' : 'hv-note-ok';
|
|
394
|
+
|
|
395
|
+
const wrap = document.createElement('div');
|
|
396
|
+
wrap.className = 'hv-wrap';
|
|
397
|
+
|
|
398
|
+
const metaHtml = (modelLine || stealPct !== null) ? `
|
|
399
|
+
<div class="hv-cpu-info">
|
|
400
|
+
${modelLine ? `<span>${h(modelLine.trim())}</span>` : ''}
|
|
401
|
+
<span><b>${nproc}</b> logical CPU${nproc !== 1 ? 's' : ''}</span>
|
|
402
|
+
${stealPct !== null
|
|
403
|
+
? `<span class="hv-steal-val ${parseFloat(stealPct) > 5 ? 'hv-warn' : 'hv-ok'}">CPU steal: <b>${stealPct}%</b>${parseFloat(stealPct) > 5 ? ' ⚠' : ''}</span>`
|
|
404
|
+
: ''}
|
|
405
|
+
</div>` : '';
|
|
406
|
+
|
|
407
|
+
const cardsHtml = `
|
|
408
|
+
<div class="hv-load-cards">
|
|
409
|
+
${[['1-min', load1], ['5-min', load5], ['15-min', load15]].map(([lbl, v]) => `
|
|
410
|
+
<div class="hv-load-card ${loadCardCls(v)}">
|
|
411
|
+
<div class="hv-load-val ${loadColor(v)}">${v.toFixed(2)}</div>
|
|
412
|
+
<div class="hv-load-lbl">${lbl} load avg</div>
|
|
413
|
+
</div>`).join('')}
|
|
414
|
+
<div class="hv-load-card hv-load-card-ok">
|
|
415
|
+
<div class="hv-load-val">${nproc}</div>
|
|
416
|
+
<div class="hv-load-lbl">CPU cores</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="hv-load-note ${loadNoteCls}">${h(loadStatus)}</div>`;
|
|
420
|
+
|
|
421
|
+
let tableHtml = '';
|
|
422
|
+
if (procLines.length) {
|
|
423
|
+
const tableRows = procLines.slice(0, 12).map(line => {
|
|
424
|
+
const p = line.trim().split(/\s+/);
|
|
425
|
+
// ps aux: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND...
|
|
426
|
+
const [user, pid, cpu, mem, vsz, rss, tty, stat, start, time, ...cmdParts] = p;
|
|
427
|
+
const cmd = cmdParts.join(' ');
|
|
428
|
+
const highCpu = parseFloat(cpu) > 50;
|
|
429
|
+
return `<tr>
|
|
430
|
+
<td>${h(user)}</td>
|
|
431
|
+
<td>${h(pid)}</td>
|
|
432
|
+
<td class="${highCpu ? 'hv-warn' : ''}" style="font-weight:${highCpu ? 700 : 400}">${h(cpu)}%</td>
|
|
433
|
+
<td>${h(mem)}%</td>
|
|
434
|
+
<td>${h(rss)}</td>
|
|
435
|
+
<td class="hv-proc-cmd">${h(cmd)}</td>
|
|
436
|
+
</tr>`;
|
|
437
|
+
}).join('');
|
|
438
|
+
|
|
439
|
+
tableHtml = `
|
|
440
|
+
<div class="hv-top-table-wrap">
|
|
441
|
+
<table class="hv-top-table">
|
|
442
|
+
<thead><tr><th>USER</th><th>PID</th><th>%CPU</th><th>%MEM</th><th>RSS</th><th>COMMAND</th></tr></thead>
|
|
443
|
+
<tbody>${tableRows}</tbody>
|
|
444
|
+
</table>
|
|
445
|
+
</div>`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
wrap.innerHTML = metaHtml + cardsHtml + tableHtml;
|
|
449
|
+
container.innerHTML = '';
|
|
450
|
+
container.className = '';
|
|
451
|
+
container.appendChild(wrap);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ── Exports ─────────────────────────────────────────────────────────── */
|
|
455
|
+
window.renderMemInfoView = renderMemInfoView;
|
|
456
|
+
window.renderMemPressureView = renderMemPressureView;
|
|
457
|
+
window.renderOomKillsView = renderOomKillsView;
|
|
458
|
+
window.renderKubeletLogsView = renderKubeletLogsView;
|
|
459
|
+
window.renderDiskView = renderDiskView;
|
|
460
|
+
window.renderCpuView = renderCpuView;
|
|
461
|
+
|
|
462
|
+
})();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>k8s node debugger</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="stylesheet"
|
|
9
|
+
href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
|
|
10
|
+
/>
|
|
11
|
+
<link rel="stylesheet" href="/style.css" />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<header id="topbar">
|
|
15
|
+
<div class="brand">
|
|
16
|
+
<span class="logo">⬡</span>
|
|
17
|
+
<span>k8s node debugger</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="session" class="session"></div>
|
|
20
|
+
<div class="actions">
|
|
21
|
+
<button id="refresh-all" title="Re-run all probes">↻ Refresh all</button>
|
|
22
|
+
<button id="snapshot-btn" title="Download JSON snapshot of all loaded probe data">⬇ Snapshot</button>
|
|
23
|
+
<a class="tb-gh-link" href="https://github.com/goutamtadi1/k8s-node-debugger" target="_blank" rel="noopener" title="View on GitHub">
|
|
24
|
+
<svg height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
25
|
+
GitHub
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<div id="layout">
|
|
31
|
+
<nav id="sidebar"></nav>
|
|
32
|
+
<main id="main">
|
|
33
|
+
<section id="overview" class="panel active">
|
|
34
|
+
<h2>Overview</h2>
|
|
35
|
+
<div id="node-card" class="card">Loading node info…</div>
|
|
36
|
+
</section>
|
|
37
|
+
<section id="probe-panels"></section>
|
|
38
|
+
|
|
39
|
+
<!-- Connectivity prober — not a probe, interactive panel -->
|
|
40
|
+
<section id="connectivity-panel" class="panel">
|
|
41
|
+
<h2>Connectivity prober <small>— test reachability from this node</small></h2>
|
|
42
|
+
<div class="conn-form">
|
|
43
|
+
<input id="conn-target" type="text" placeholder="IP or hostname e.g. 10.96.0.1 or kubernetes.default" autocomplete="off" spellcheck="false" />
|
|
44
|
+
<input id="conn-port" type="text" placeholder="Port e.g. 443" style="width:110px" autocomplete="off" />
|
|
45
|
+
<select id="conn-proto">
|
|
46
|
+
<option value="tcp">TCP</option>
|
|
47
|
+
<option value="http">HTTP</option>
|
|
48
|
+
<option value="https">HTTPS</option>
|
|
49
|
+
<option value="udp">UDP</option>
|
|
50
|
+
</select>
|
|
51
|
+
<button id="conn-run">Test ▶</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="conn-hint">
|
|
54
|
+
Runs ping · nc/curl · DNS resolution · traceroute from inside the debug pod (host network namespace),
|
|
55
|
+
then shows matching conntrack entries for the target.
|
|
56
|
+
</div>
|
|
57
|
+
<div id="conn-results"></div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section id="terminal-panel" class="panel">
|
|
61
|
+
<h2>Terminal <small>— runs inside the debug pod (hostNetwork)</small></h2>
|
|
62
|
+
<div class="term-toolbar">
|
|
63
|
+
<input
|
|
64
|
+
id="term-input"
|
|
65
|
+
type="text"
|
|
66
|
+
placeholder="e.g. tcpdump -ni any port 53 · dig kubernetes.default · conntrack -E"
|
|
67
|
+
autocomplete="off"
|
|
68
|
+
spellcheck="false"
|
|
69
|
+
/>
|
|
70
|
+
<button id="term-run">Run ▶</button>
|
|
71
|
+
<button id="term-stop" title="Send SIGINT">Stop ◼</button>
|
|
72
|
+
<button id="term-clear">Clear</button>
|
|
73
|
+
</div>
|
|
74
|
+
<div id="term"></div>
|
|
75
|
+
<div class="hint">
|
|
76
|
+
Press <kbd>Enter</kbd> to run · <kbd>Ctrl-C</kbd> in the box (or Stop)
|
|
77
|
+
interrupts a running command · <kbd>↑/↓</kbd> for history.
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
</main>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
84
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
85
|
+
<script src="/iptables-view.js"></script>
|
|
86
|
+
<script src="/conntrack-view.js"></script>
|
|
87
|
+
<script src="/health-view.js"></script>
|
|
88
|
+
<script src="/app.js"></script>
|
|
89
|
+
</body>
|
|
90
|
+
</html>
|