新增 glance 进程监控命令:采集 cpu/内存/句柄数并生成报告
TDevOpsCICD / build-kit (push) Successful in 5m54s

This commit is contained in:
cheney
2026-09-18 10:00:15 +08:00
parent 593ac96c8c
commit 072c9bfa50
7 changed files with 713 additions and 4 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ module.exports = class CliCmdLine {
let param = cmdLine.params[pname]; let param = cmdLine.params[pname];
if ( param.isFlag ) { if ( param.isFlag ) {
if ( param.needValue ){ if ( param.needValue ){
if ( params[pname] ) { if ( params[pname] !== undefined ) {
this.params[pname] = params[pname] this.params[pname] = params[pname]
} else { } else {
if ( ! param.isOptional ) { if ( ! param.isOptional ) {
+11 -3
View File
@@ -85,12 +85,21 @@ module.exports = class CliCmdParser {
// 位置参数:值 // 位置参数:值
const position = [] const position = []
let line = this.cmdLines[ argv[0] ];
for (let i = 1; i < argv.length; i++ ) { for (let i = 1; i < argv.length; i++ ) {
if ( argv[i].startsWith("--") ) { if ( argv[i].startsWith("--") ) {
const name = argv[i].substring(2);
if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) { if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) {
throw "flag 格式错误" + argv[i]; const param = line.params[name];
if ( ! param || ! param.isOptional ) {
throw "flag 格式错误" + argv[i];
}
// 可选命名参数允许不带值,值为空串,由命令自行做默认值
params[ name ] = "";
} else {
params[ name ] = argv[++i];
} }
params[ argv[i].substring(2) ] = argv[++i];
} else if ( argv[i].startsWith("-") ) { } else if ( argv[i].startsWith("-") ) {
flags.add( argv[i].substring(1) ); flags.add( argv[i].substring(1) );
@@ -99,7 +108,6 @@ module.exports = class CliCmdParser {
} }
} }
let line = this.cmdLines[ argv[0] ];
// 整理 // 整理
// line 是藐视 // line 是藐视
// flags, params, position 是值 // flags, params, position 是值
+168
View File
@@ -0,0 +1,168 @@
/**
* kit glance 命令
* - 记录模式:--pid <pid> 或 --port <port>,采集一次写入 sqlite(~/.kit/glance.db)
* - 报告模式:--report [报告文件](默认 report.html),需配合 --pid/--port
* - --startTime/--endTime 格式 yyyyMMddHHmmss,指定报告时间范围
*/
const fs = require("fs");
const path = require("path");
const store = require("./store");
const report = require("./report");
const monitor = require("./monitor");
const HOUR = 60 * 60 * 1000;
const HOURS_24 = 24 * HOUR;
function parseTime(text) {
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(text);
if (!m) {
return null;
}
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]));
return d.getTime();
}
function resolveRange(startText, endText) {
if (startText && endText) {
const s = parseTime(startText);
const e = parseTime(endText);
if (s == null || e == null) {
return { error: "--startTime/--endTime 格式应为 yyyyMMddHHmmss" };
}
if (e <= s) {
return { error: "--endTime 必须晚于 --startTime" };
}
return { start: s, end: e };
}
const now = Date.now();
if (startText) {
const s = parseTime(startText);
if (s == null) {
return { error: "--startTime 格式应为 yyyyMMddHHmmss" };
}
return { start: s, end: s + HOURS_24 };
}
if (endText) {
const e = parseTime(endText);
if (e == null) {
return { error: "--endTime 格式应为 yyyyMMddHHmmss" };
}
return { start: e - HOURS_24, end: e };
}
return { start: now - HOURS_24, end: now };
}
async function record(cli) {
const db = store.open();
let record = null;
const pidText = cli.getParamValue("pid");
if (pidText && pidText !== "") {
const pid = Number(pidText);
if (!Number.isInteger(pid) || pid <= 0) {
$logger.error(`pid 参数无效: ${pidText}`);
process.exit(-1);
}
const data = await monitor.collectPid(pid);
if (!data) {
$logger.warn(`进程 ${pid} 不存在或已退出,本次不记录`);
return;
}
record = { ts: Date.now(), target: "pid", pid, port: null, cpu: data.cpu, mem: data.mem, handle: data.handle };
} else {
const portText = cli.getParamValue("port");
const port = Number(portText);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
$logger.error(`port 参数无效: ${portText}`);
process.exit(-1);
}
const found = monitor.findPortPid(port);
if (!found) {
$logger.warn(`端口 ${port} 无监听进程,本次不记录`);
return;
}
const data = await monitor.collectPid(found);
if (!data) {
$logger.warn(`进程 ${found} 已退出,本次不记录`);
return;
}
record = { ts: Date.now(), target: "port", pid: found, port, cpu: data.cpu, mem: data.mem, handle: data.handle };
}
store.insert(db, record);
$logger.info(`已记录 target=${record.target} pid=${record.pid} cpu=${record.cpu}% mem=${record.mem}MB handle=${record.handle}`);
}
function buildReport(cli, reportFile) {
const isPid = cli.getParamValue("pid") != null;
const { start, end, error } = resolveRange(cli.getParamValue("startTime"), cli.getParamValue("endTime"));
if (error) {
$logger.error(error);
process.exit(-1);
}
const db = store.open();
const rows = isPid
? store.queryRange(db, { pid: Number(cli.getParamValue("pid")), start, end })
: store.queryRange(db, { port: Number(cli.getParamValue("port")), start, end });
const html = report.render({
mode: isPid ? "pid" : "port",
value: isPid ? Number(cli.getParamValue("pid")) : Number(cli.getParamValue("port")),
rows,
start,
end
});
fs.writeFileSync(reportFile, html, "utf8");
$logger.info(`报告已生成: ${path.resolve(reportFile)}`);
}
async function main(cli) {
const pidText = cli.getParamValue("pid");
const portText = cli.getParamValue("port");
const reportRaw = cli.getParamValue("report");
const isPid = pidText != null && pidText !== "";
const isPort = portText != null && portText !== "";
const isReport = reportRaw !== undefined;
if (isPid && isPort) {
$logger.error("--pid 和 --port 只能传递一个");
process.exit(-1);
}
if (!isPid && !isPort) {
$logger.error("--pid 和 --port 必须传递一个");
process.exit(-1);
}
if (isPid) {
const pid = Number(pidText);
if (!Number.isInteger(pid) || pid <= 0) {
$logger.error(`pid 参数无效: ${pidText}`);
process.exit(-1);
}
} else if (isPort) {
const port = Number(portText);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
$logger.error(`port 参数无效: ${portText}`);
process.exit(-1);
}
}
if (isReport) {
const reportFile = reportRaw || "report.html";
buildReport(cli, reportFile);
return;
}
if (!monitor.isLinux()) {
$logger.error("glance 仅支持 Linux 操作系统");
process.exit(-1);
}
await record(cli);
}
module.exports = { main };
+148
View File
@@ -0,0 +1,148 @@
/**
* 进程监控指标采集(仅支持 Linux)
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
* - 句柄数:/proc/<pid>/fd 目录条目数
* - 端口 -> pid:解析 /proc/net/tcp 与 /proc/net/tcp6,结合 /proc/<pid>/fd 的 socket inode
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
function isLinux() {
return os.type() === "Linux";
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function readPidTicks(pid) {
const txt = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
// 字段 14=utime 15=stime,comm 可能含空格,按最后一个 ) 截断
const rest = txt.slice(txt.lastIndexOf(")") + 1).trim().split(/\s+/);
return Number(rest[11]) + Number(rest[12]);
}
function readCpuTotal() {
const line = fs.readFileSync("/proc/stat", "utf8").split("\n")[0];
// user nice system idle iowait irq softirq steal(guest 已计入 user)
const parts = line.trim().split(/\s+/).slice(1, 9).map(Number);
let total = 0;
for (const v of parts) {
total += Number.isFinite(v) ? v : 0;
}
return total;
}
function readMemMB(pid) {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
if (!m) {
return null;
}
return Math.round(Number(m[1]) / 1024 * 100) / 100;
}
function readHandleCount(pid) {
return fs.readdirSync(`/proc/${pid}/fd`, { withFileTypes: true }).length;
}
/**
* 采集单个进程的监控数据
* @param {number} pid
* @returns {Promise<{pid:number,cpu:number,mem:number,handle:number}|null>} 进程不存在/不可读返回 null
*/
async function collectPid(pid) {
try {
const beforePid = readPidTicks(pid);
const beforeTotal = readCpuTotal();
await sleep(1000);
const afterPid = readPidTicks(pid);
const afterTotal = readCpuTotal();
const totalDelta = afterTotal - beforeTotal;
const pidDelta = afterPid - beforePid;
const cpu = totalDelta > 0 ? Math.round(pidDelta / totalDelta * 10000) / 100 : 0;
const mem = readMemMB(pid);
const handle = readHandleCount(pid);
return { pid, cpu, mem, handle };
} catch (e) {
return null;
}
}
/**
* 根据端口查找监听进程 pid
* @param {number} port
* @returns {number|null}
*/
function findPortPid(port) {
const targetInodes = new Set();
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
let text;
try {
text = fs.readFileSync(file, "utf8");
} catch (e) {
continue;
}
const lines = text.split("\n").slice(1);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 10) {
continue;
}
// st == 0A 表示 LISTEN
if (parts[3] !== "0A") {
continue;
}
const local = parts[1];
const idx = local.lastIndexOf(":");
if (idx < 0) {
continue;
}
const realPort = parseInt(local.slice(idx + 1), 16);
if (Number.isFinite(realPort) && realPort === Number(port)) {
targetInodes.add(parts[9]);
}
}
}
if (targetInodes.size === 0) {
return null;
}
const entries = fs.readdirSync("/proc");
for (const entry of entries) {
if (!/^\d+$/.test(entry)) {
continue;
}
const fdDir = `/proc/${entry}/fd`;
let names;
try {
names = fs.readdirSync(fdDir);
} catch (e) {
continue;
}
for (const fd of names) {
try {
const target = fs.readlinkSync(path.join(fdDir, fd));
const m = target.match(/^socket:\[(\d+)\]$/);
if (m && targetInodes.has(m[1])) {
return Number(entry);
}
} catch (e) {
// 忽略无法读取的 fd
}
}
}
return null;
}
module.exports = {
isLinux,
collectPid,
findPortPid
};
+312
View File
@@ -0,0 +1,312 @@
/**
* glance 报告生成:生成一个自包含 HTML 文件(内联 SVG 图表,无外部依赖)。
* 三张图表:CPU 占用率、内存占用、句柄数。
* - 纵轴/横轴根据数据量自动缩放
* - 时间段内无数据或进程中停(时间缺口)时,图表会显现
* - pid 变化(进程重启)以橙色虚线 + 标注标出
*/
const HOUR = 60 * 60 * 1000;
function esc(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function fmtTime(ts) {
const d = new Date(ts);
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}
function fmtFull(ts) {
const d = new Date(ts);
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
function niceStep(raw) {
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
const norm = raw / mag;
let step;
if (norm <= 1) step = 1;
else if (norm <= 2) step = 2;
else if (norm <= 5) step = 5;
else step = 10;
return step * mag;
}
function computeYAxis(values) {
const vs = values.filter(v => Number.isFinite(v));
if (!vs.length) {
return { min: 0, max: 1, ticks: [0, 0.2, 0.4, 0.6, 0.8, 1] };
}
let min = Math.min(...vs);
let max = Math.max(...vs);
if (min === max) {
min -= 1;
max += 1;
}
const step = niceStep((max - min) / 5);
const lo = Math.floor(min / step) * step;
const hi = Math.ceil(max / step) * step;
const ticks = [];
for (let v = lo; v <= hi + step / 2; v += step) {
ticks.push(Math.round(v * 1e6) / 1e6);
}
return { min: lo, max: hi, ticks };
}
/**
* 生成折线内容:按时间缺口切成多段 polyline,缺口处绘制淡色竖带
*/
function buildLine(entries, X, Y, gapThreshold, color) {
let svg = "";
let segment = [];
const gaps = [];
function flush() {
if (segment.length > 0) {
const points = segment.map(p => `${X(p.ts).toFixed(2)},${Y(p.value).toFixed(2)}`).join(" ");
svg += `\n <polyline fill="none" stroke="${color}" stroke-width="2" stroke-linejoin="round" points="${points}"/>`;
for (const p of segment) {
svg += `\n <circle cx="${X(p.ts).toFixed(2)}" cy="${Y(p.value).toFixed(2)}" r="2.5" fill="${color}"/>`;
}
segment = [];
}
}
for (let i = 0; i < entries.length; i++) {
if (i > 0 && gapThreshold > 0) {
const dt = entries[i].ts - entries[i - 1].ts;
if (dt > gapThreshold) {
gaps.push({ x0: X(entries[i - 1].ts), x1: X(entries[i].ts) });
flush();
}
}
segment.push(entries[i]);
}
flush();
return { svg, gaps };
}
function buildChart(title, unit, color, rows, start, end, gapThreshold, pidChanges) {
const W = 960, H = 300, mL = 80, mR = 40, mT = 36, mB = 52;
const pw = W - mL - mR;
const ph = H - mT - mB;
const span = (end - start) || 1;
const entries = rows
.filter(r => r.value != null && Number.isFinite(r.value))
.map(r => ({ ts: r.ts, value: r.value, pid: r.pid }));
const X = ts => mL + Math.max(0, Math.min(1, (ts - start) / span)) * pw;
const axis = computeYAxis(entries.map(e => e.value));
const Y = v => mT + ph - ((v - axis.min) / ((axis.max - axis.min) || 1)) * ph;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="100%" height="${H}" role="img">`;
svg += `\n <text x="${mL}" y="22" font-size="13" font-weight="600" fill="#333">${esc(title)}${unit ? `(${esc(unit)})` : ""}</text>`;
// 网格与纵轴
for (const t of axis.ticks) {
const y = Y(t);
svg += `\n <line x1="${mL}" y1="${y.toFixed(2)}" x2="${W - mR}" y2="${y.toFixed(2)}" stroke="#e5e7eb" stroke-width="1"/>`;
svg += `\n <text x="${mL - 8}" y="${(y + 4).toFixed(2)}" font-size="11" fill="#6b7280" text-anchor="end">${t}</text>`;
}
// 横轴(6 个刻度)
for (let k = 0; k <= 5; k++) {
const ts = start + (end - start) * k / 5;
const x = X(ts);
svg += `\n <line x1="${x.toFixed(2)}" y1="${mT + ph}" x2="${x.toFixed(2)}" y2="${mT + ph + 4}" stroke="#9ca3af" stroke-width="1"/>`;
svg += `\n <text x="${x.toFixed(2)}" y="${mT + ph + 18}" font-size="11" fill="#6b7280" text-anchor="middle">${fmtTime(ts)}</text>`;
}
// 坐标轴
svg += `\n <line x1="${mL}" y1="${mT}" x2="${mL}" y2="${mT + ph}" stroke="#9ca3af" stroke-width="1"/>`;
svg += `\n <line x1="${mL}" y1="${mT + ph}" x2="${W - mR}" y2="${mT + ph}" stroke="#9ca3af" stroke-width="1"/>`;
if (entries.length === 0) {
svg += `\n <text x="${mL + pw / 2}" y="${mT + ph / 2}" font-size="14" fill="#9ca3af" text-anchor="middle">该时间范围无数据</text>`;
} else {
const { svg: lineSvg, gaps } = buildLine(entries, X, Y, gapThreshold, color);
svg += lineSvg;
// 时间缺口高亮(一段时间没数据)
for (const g of gaps) {
const x = Math.min(g.x0, g.x1);
const w = Math.max(1, Math.abs(g.x1 - g.x0));
svg += `\n <rect x="${x.toFixed(2)}" y="${(mT + 6).toFixed(2)}" width="${w.toFixed(2)}" height="${(ph - 12).toFixed(2)}" fill="#f3f4f6" stroke="#d1d5db" stroke-width="0.5"/>`;
}
// pid 变化(进程重启)竖线
if (pidChanges && pidChanges.length) {
for (const ch of pidChanges) {
const x = X(ch.ts);
svg += `\n <line x1="${x.toFixed(2)}" y1="${mT}" x2="${x.toFixed(2)}" y2="${mT + ph}" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,3"/>`;
const label = `pid ${ch.from}→${ch.to}`;
const lx = Math.min(x + 5, W - mR - 110);
const ly = mT + 14;
svg += `\n <text x="${lx.toFixed(2)}" y="${ly}" font-size="11" fill="#b45309" stroke="#ffffff" stroke-width="3" paint-order="stroke">↻ ${esc(label)}</text>`;
}
}
}
svg += "\n</svg>";
return svg;
}
function computeGapThreshold(rows) {
const tsList = rows
.map(r => r.ts)
.filter(ts => Number.isFinite(ts))
.sort((a, b) => a - b);
if (tsList.length < 2) {
return 0;
}
const deltas = [];
for (let i = 1; i < tsList.length; i++) {
const d = tsList[i] - tsList[i - 1];
if (d > 0) {
deltas.push(d);
}
}
if (!deltas.length) {
return 0;
}
deltas.sort((a, b) => a - b);
const typical = deltas[Math.floor(deltas.length / 2)];
return Math.max(typical * 3, 30 * 1000);
}
function collectPidChanges(rows) {
const changes = [];
for (let i = 1; i < rows.length; i++) {
const prev = rows[i - 1].pid;
const cur = rows[i].pid;
if (prev != null && cur != null && prev !== cur) {
changes.push({ ts: rows[i].ts, from: prev, to: cur });
}
}
return changes;
}
function summary(rows, key) {
const vs = rows.map(r => r[key]).filter(v => v != null && Number.isFinite(v));
if (!vs.length) {
return null;
}
const min = Math.min(...vs);
const max = Math.max(...vs);
const avg = vs.reduce((a, b) => a + b, 0) / vs.length;
return { min, max, avg };
}
function fmtNum(v, digits) {
if (v == null || !Number.isFinite(v)) {
return "-";
}
return Number(v.toFixed(digits));
}
/**
* 生成报告 HTML
* @param opts {mode:'pid'|'port', value, rows, start, end}
*/
function render(opts) {
const { mode, value, rows, start, end } = opts;
const spanHours = Math.round((end - start) / HOUR * 10) / 10;
const targetText = mode === "pid" ? `--pid ${value}` : `--port ${value}`;
const gapThreshold = computeGapThreshold(rows);
const pidChanges = collectPidChanges(rows);
const restarts = pidChanges.length;
const generatedAt = fmtFull(Date.now());
const series = [
{ key: "cpu", title: "CPU 占用率", unit: "%", color: "#2563eb" },
{ key: "mem", title: "内存占用", unit: "MB", color: "#16a34a" },
{ key: "handle", title: "句柄数", unit: "", color: "#dc2626" }
];
const summaryRows = series.map(s => {
const st = summary(rows, s.key);
return {
title: s.title,
unit: s.unit,
min: st ? fmtNum(st.min, 2) : "-",
max: st ? fmtNum(st.max, 2) : "-",
avg: st ? fmtNum(st.avg, 2) : "-"
};
});
let chartsHtml = "";
for (const s of series) {
chartsHtml += `
<section class="chart">
<h2>${esc(s.title)}</h2>
${buildChart(s.title, s.unit, s.color, rows.map(r => ({ ts: r.ts, value: r[s.key], pid: r.pid })), start, end, gapThreshold, pidChanges)}
</section>`;
}
const summaryHtml = summaryRows.map(r => `
<tr>
<td>${esc(r.title)}${r.unit ? `(${esc(r.unit)})` : ""}</td>
<td>${r.min}</td>
<td>${r.max}</td>
<td>${r.avg}</td>
</tr>`).join("");
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>进程监控报告 ${esc(targetText)}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: #f9fafb; margin: 0; padding: 24px; color: #1f2937; }
.report { max-width: 1040px; margin: 0 auto; }
header { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px 24px; margin-bottom: 16px; }
header h1 { margin: 0 0 8px; font-size: 22px; }
.meta { display: flex; flex-wrap: wrap; gap: 8px 32px; font-size: 13px; color: #4b5563; }
.meta b { color: #111827; }
.charts { display: flex; flex-direction: column; gap: 16px; }
.chart { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px 8px; }
.chart h2 { margin: 0 0 4px; font-size: 15px; color: #374151; }
.legend { color: #6b7280; font-size: 12px; margin-top: 4px; }
table { border-collapse: collapse; margin-top: 12px; font-size: 13px; width: 100%; }
th, td { text-align: right; padding: 6px 12px; border-bottom: 1px solid #e5e7eb; }
th:first-child, td:first-child { text-align: left; }
th { color: #6b7280; font-weight: 500; }
footer { margin-top: 16px; font-size: 12px; color: #9ca3af; text-align: center; }
</style>
</head>
<body>
<div class="report">
<header>
<h1>进程监控报告</h1>
<div class="meta">
<span>监控目标:<b>${esc(targetText)}</b></span>
<span>时间范围:<b>${fmtFull(start)} ~ ${fmtFull(end)}</b>(约 ${spanHours} 小时)</span>
<span>样本数:<b>${rows.length}</b></span>
<span>重启次数:<b>${restarts}</b></span>
<span>生成时间:<b>${generatedAt}</b></span>
</div>
<table>
<thead><tr><th>指标</th><th>最小值</th><th>最大值</th><th>平均值</th></tr></thead>
<tbody>${summaryHtml}</tbody>
</table>
<div class="legend">说明:时间轴覆盖请求范围;灰色竖带表示其间无采样数据(进程中断/采集缺失);橙色虚线表示 pid 变化(进程重启)。</div>
</header>
<div class="charts">
${chartsHtml}
</div>
<footer>generated by kit glance</footer>
</div>
</body>
</html>`;
}
module.exports = { render };
+68
View File
@@ -0,0 +1,68 @@
/**
* glance 数据存储:默认 sqlite 位于 ~/.kit/glance.db(不存在则创建)
* 依赖 bun:sqlite,仅在调用时加载,避免影响 node 环境下的其他命令。
*/
const fs = require("fs");
const os = require("os");
const path = require("path");
function defaultDbPath() {
return path.join(os.homedir(), ".kit", "glance.db");
}
function open(dbPath) {
dbPath = dbPath || defaultDbPath();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const { Database } = require("bun:sqlite");
const db = new Database(dbPath);
db.exec(`CREATE TABLE IF NOT EXISTS glance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
target TEXT NOT NULL,
pid INTEGER,
port INTEGER,
cpu REAL,
mem REAL,
handle INTEGER
)`);
db.exec("CREATE INDEX IF NOT EXISTS idx_glance_lookup ON glance(pid, port, ts)");
return db;
}
/**
* 插入一条监控记录
* @param db database
* @param record {ts, target, pid, port, cpu, mem, handle}
*/
function insert(db, record) {
db.query("INSERT INTO glance (ts, target, pid, port, cpu, mem, handle) VALUES (?, ?, ?, ?, ?, ?, ?)")
.run(record.ts, record.target, record.pid ?? null, record.port ?? null, record.cpu ?? null, record.mem ?? null, record.handle ?? null);
}
/**
* 按时间范围查询监控数据(忽略 target,pid/port 二者取其一)
* @param db database
* @param opts {pid?, port?, start, end}
* @returns rows
*/
function queryRange(db, opts) {
let where;
const params = [];
if (opts.pid != null) {
where = "pid = ?";
params.push(opts.pid);
} else {
where = "port = ?";
params.push(opts.port);
}
params.push(opts.start, opts.end);
return db.query(`SELECT ts, target, pid, port, cpu, mem, handle FROM glance WHERE ${where} AND ts >= ? AND ts < ? ORDER BY ts ASC`).all(...params);
}
module.exports = {
defaultDbPath,
open,
insert,
queryRange
};
+5
View File
@@ -15,6 +15,7 @@ const zlib = require("zlib");
const mem = require("./mem") const mem = require("./mem")
const { getAllDeviceInfo } = require("./platform") const { getAllDeviceInfo } = require("./platform")
const kitcommand = require("./kitcommand") const kitcommand = require("./kitcommand")
const glance = require("./glance")
kitcommand.init() kitcommand.init()
kitcommand.regTo( parser ) kitcommand.regTo( parser )
@@ -188,6 +189,10 @@ parser.addCmdLine("killport <port>", "释放端口", function (cli) {
net.killport( parseInt(port, 10) ); net.killport( parseInt(port, 10) );
}); });
parser.addCmdLine("glance [--pid] [--port] [--report] [--startTime] [--endTime]", "进程监控:--pid/--port 采集监控数据(仅 Linux),--report 生成报告并输出 HTML;", async function (cli) {
await glance.main(cli);
});
parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) { parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) {
const getPort = await import('get-port').then(psl => psl.default); const getPort = await import('get-port').then(psl => psl.default);