@hyzyn/dsh-docker 0.13.6 → 0.14.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/README.en.md +192 -7
- package/README.md +162 -7
- package/client-src/docker.css +477 -0
- package/client-src/index.js +1562 -12
- package/client.js +505 -25
- package/lib/diagnose.d.ts +148 -0
- package/lib/diagnose.js +571 -0
- package/lib/diagnose.js.map +1 -0
- package/lib/docker.d.ts +257 -2
- package/lib/docker.js +464 -4
- package/lib/docker.js.map +1 -1
- package/lib/history-store.d.ts +48 -0
- package/lib/history-store.js +187 -0
- package/lib/history-store.js.map +1 -0
- package/lib/history.d.ts +110 -0
- package/lib/history.js +271 -0
- package/lib/history.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +778 -1
- package/lib/index.js.map +1 -1
- package/lib/recreate.d.ts +49 -0
- package/lib/recreate.js +305 -0
- package/lib/recreate.js.map +1 -0
- package/package.json +4 -4
- package/scripts/client-smoke.mjs +916 -0
- package/scripts/route-smoke.mjs +289 -8
package/lib/history.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-docker — 诊断历史:把「一次诊断」压成可比较的样本,并把样本读成趋势(纯逻辑,零 IO)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么样本里只有数字和 code
|
|
5
|
+
*
|
|
6
|
+
* 诊断结果里有两类东西**绝不能落盘**:容器日志原文(`Diagnosis.logTail`)与每条 finding 的
|
|
7
|
+
* `detail`(里面会嵌日志行,可能带凭证 / token)。所以样本只留**结构化信号**——结论、判据 code
|
|
8
|
+
* 列表、几个关键数字、时间。这与本包既有取向一致:审计行只记 `key=value` 摘要(exec 命令截断
|
|
9
|
+
* 进日志)、密码永不回传浏览器。`sampleFromDiagnosis()` 是这条边界的**唯一执行点**,测试里有一条
|
|
10
|
+
* 「序列化后不含日志片段」的断言盯着它。
|
|
11
|
+
*
|
|
12
|
+
* ## 为什么这个文件不读时钟
|
|
13
|
+
*
|
|
14
|
+
* 同 `diagnose.ts`:判据要被逐条「正例 + 反例」单测,时钟就必须从外面传进来(`nowMs`)。
|
|
15
|
+
* 时间戳的格式化也只发生在调用方——见 `sampleFromDiagnosis(diagnosis, nowMs)` 的注释。
|
|
16
|
+
*
|
|
17
|
+
* ## 三种「没有」仍然可区分
|
|
18
|
+
*
|
|
19
|
+
* 样本里 `memPercent` / `errorLines` / `restartCount` 等可以是 `null`(本次没采到)。
|
|
20
|
+
* 趋势判据遇到 `null` **不当作 0**:两端有一端是 `null` 就跳过这条判据(宁可不说,
|
|
21
|
+
* 也不把「没采到」读成「没变化」)。
|
|
22
|
+
*/
|
|
23
|
+
import type { Diagnosis, DiagnosisSeverity } from './diagnose.js';
|
|
24
|
+
/** 每个容器保留的样本上限(更早的丢最旧)。 */
|
|
25
|
+
export declare const HISTORY_PER_CONTAINER = 50;
|
|
26
|
+
/** 整个历史文件的样本上限(超出时按「旧行优先丢」重写文件)。 */
|
|
27
|
+
export declare const HISTORY_MAX_LINES = 2000;
|
|
28
|
+
/**
|
|
29
|
+
* 去重窗口:同一容器、距上一条 < 30s、且签名完全相同 → 不追加。
|
|
30
|
+
*
|
|
31
|
+
* 为什么需要:面板上「重新诊断」是可以连点的,而诊断结果在几十秒内通常一模一样。
|
|
32
|
+
* 不设窗口的话,趋势里会凭空出现一串「同一状态」的样本,把「它在稳定」和「它刚变了」
|
|
33
|
+
* 搅成同一根曲线;同时每次连点都要重写一次文件(IO 放大)。
|
|
34
|
+
*/
|
|
35
|
+
export declare const HISTORY_DEDUPE_MS = 30000;
|
|
36
|
+
/** 一次诊断压成的历史样本。**只存结论与数字**,见文件头「为什么样本里只有数字和 code」。 */
|
|
37
|
+
export interface DiagnosisSample {
|
|
38
|
+
/** ISO 8601(毫秒精度)。 */
|
|
39
|
+
at: string;
|
|
40
|
+
severity: DiagnosisSeverity;
|
|
41
|
+
/** verdict.code(`running-ok` / `oom-killed` / …)。 */
|
|
42
|
+
code: string;
|
|
43
|
+
/** 判据 code 列表(排序去重);**不含** title / detail。 */
|
|
44
|
+
findings: string[];
|
|
45
|
+
state: string;
|
|
46
|
+
exitCode: number | null;
|
|
47
|
+
oomKilled: boolean;
|
|
48
|
+
restartCount: number | null;
|
|
49
|
+
memPercent: number | null;
|
|
50
|
+
cpuPercent: number | null;
|
|
51
|
+
/** 采集失败给 `null`(与「0 行」区分,同 diagnose.ts 的三种「没有」)。 */
|
|
52
|
+
errorLines: number | null;
|
|
53
|
+
eventCount: number | null;
|
|
54
|
+
logLines: number | null;
|
|
55
|
+
}
|
|
56
|
+
export interface HistoryNumber {
|
|
57
|
+
/** 稳定标识(客户端按它做 i18n 标签,**不要**在这里放中文)。 */
|
|
58
|
+
key: string;
|
|
59
|
+
from: number;
|
|
60
|
+
to: number;
|
|
61
|
+
delta: number;
|
|
62
|
+
}
|
|
63
|
+
export interface HistoryTrend {
|
|
64
|
+
count: number;
|
|
65
|
+
/** 窗口的首尾时间(样本 <2 时为 `null`)。 */
|
|
66
|
+
window: {
|
|
67
|
+
from: string;
|
|
68
|
+
to: string;
|
|
69
|
+
} | null;
|
|
70
|
+
verdict: {
|
|
71
|
+
severity: DiagnosisSeverity;
|
|
72
|
+
code: string;
|
|
73
|
+
summary: string;
|
|
74
|
+
};
|
|
75
|
+
/** 只含**首尾真的变了**的项(没变、或一端为 null 的都不列)。 */
|
|
76
|
+
numbers: HistoryNumber[];
|
|
77
|
+
/** 窗口内最早一次没有、最近一次有的判据 code。 */
|
|
78
|
+
newFindings: string[];
|
|
79
|
+
/** 最早有、最近没有的判据 code。 */
|
|
80
|
+
resolvedFindings: string[];
|
|
81
|
+
/** 任一恶化判据命中(客户端据此加重整块视觉)。 */
|
|
82
|
+
aggravating: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 一次诊断 → 一条历史样本。
|
|
86
|
+
*
|
|
87
|
+
* `nowMs` 由调用方注入(理由见文件头):样本的时间戳是**这次诊断发生的时刻**,不是容器状态的时刻。
|
|
88
|
+
*
|
|
89
|
+
* 刻意**不**从 `diagnosis.evidence` 里搬 `status` / `health` 这类自由文本——它们是给人看的,
|
|
90
|
+
* 存进历史既占地方又可能带出主机细节;趋势只需要数字与 code。
|
|
91
|
+
*/
|
|
92
|
+
export declare function sampleFromDiagnosis(diagnosis: Diagnosis, nowMs: number): DiagnosisSample;
|
|
93
|
+
/**
|
|
94
|
+
* 这一条要不要追加。
|
|
95
|
+
*
|
|
96
|
+
* 只有「距上一条 < 30s **且** 签名完全相同」才丢——签名不同说明真发生了变化(容器刚被重启、
|
|
97
|
+
* 判据刚多了一条),哪怕只隔 1 秒也必须留下,否则趋势会把真正的拐点吃掉。
|
|
98
|
+
*/
|
|
99
|
+
export declare function shouldAppend(last: DiagnosisSample | undefined, next: DiagnosisSample, nowMs: number): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* 样本序列 → 趋势。
|
|
102
|
+
*
|
|
103
|
+
* 判据按**优先级**取第一条命中的作为 verdict(`aggravating` 则是「任一命中」):
|
|
104
|
+
* 重启增长 > 反复 OOM > 内存爬升 > 报错增长 > 新出现严重判据 > 结论持续变严重 > 稳定。
|
|
105
|
+
* 顺序即重要性:一个正在 crash-loop 的容器,哪怕内存也在爬,第一句话该说「它在反复重启」。
|
|
106
|
+
*
|
|
107
|
+
* 文案是**给用户看的**(同 diagnose.ts):不说 `restartCount` / `memPercent` 这类字段名,
|
|
108
|
+
* 说「重启次数」「内存占用」;每条 ≤60 字。
|
|
109
|
+
*/
|
|
110
|
+
export declare function historyTrend(samples: readonly DiagnosisSample[]): HistoryTrend;
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/** 每个容器保留的样本上限(更早的丢最旧)。 */
|
|
2
|
+
export const HISTORY_PER_CONTAINER = 50;
|
|
3
|
+
/** 整个历史文件的样本上限(超出时按「旧行优先丢」重写文件)。 */
|
|
4
|
+
export const HISTORY_MAX_LINES = 2000;
|
|
5
|
+
/**
|
|
6
|
+
* 去重窗口:同一容器、距上一条 < 30s、且签名完全相同 → 不追加。
|
|
7
|
+
*
|
|
8
|
+
* 为什么需要:面板上「重新诊断」是可以连点的,而诊断结果在几十秒内通常一模一样。
|
|
9
|
+
* 不设窗口的话,趋势里会凭空出现一串「同一状态」的样本,把「它在稳定」和「它刚变了」
|
|
10
|
+
* 搅成同一根曲线;同时每次连点都要重写一次文件(IO 放大)。
|
|
11
|
+
*/
|
|
12
|
+
export const HISTORY_DEDUPE_MS = 30_000;
|
|
13
|
+
/** 命中 `new-serious-findings` 的判据 code(这几条一旦**新出现**就是恶化信号)。 */
|
|
14
|
+
const SERIOUS_FINDINGS = new Set(['oom-killed', 'crash-loop', 'unhealthy', 'exit-nonzero']);
|
|
15
|
+
/** 严重度排序(用于 `severity-creep` 的「非降」判定)。 */
|
|
16
|
+
const SEVERITY_RANK = {
|
|
17
|
+
ok: 0,
|
|
18
|
+
info: 1,
|
|
19
|
+
warning: 2,
|
|
20
|
+
critical: 3,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 一次诊断 → 一条历史样本。
|
|
24
|
+
*
|
|
25
|
+
* `nowMs` 由调用方注入(理由见文件头):样本的时间戳是**这次诊断发生的时刻**,不是容器状态的时刻。
|
|
26
|
+
*
|
|
27
|
+
* 刻意**不**从 `diagnosis.evidence` 里搬 `status` / `health` 这类自由文本——它们是给人看的,
|
|
28
|
+
* 存进历史既占地方又可能带出主机细节;趋势只需要数字与 code。
|
|
29
|
+
*/
|
|
30
|
+
export function sampleFromDiagnosis(diagnosis, nowMs) {
|
|
31
|
+
const evidence = diagnosis.evidence;
|
|
32
|
+
return {
|
|
33
|
+
at: new Date(nowMs).toISOString(),
|
|
34
|
+
severity: diagnosis.verdict.severity,
|
|
35
|
+
code: diagnosis.verdict.code,
|
|
36
|
+
findings: [...new Set(diagnosis.findings.map((finding) => finding.code))].sort(),
|
|
37
|
+
state: evidence.state,
|
|
38
|
+
exitCode: evidence.exitCode,
|
|
39
|
+
oomKilled: evidence.oomKilled,
|
|
40
|
+
restartCount: evidence.restartCount,
|
|
41
|
+
memPercent: evidence.memPercent,
|
|
42
|
+
cpuPercent: evidence.cpuPercent,
|
|
43
|
+
errorLines: evidence.errorLines,
|
|
44
|
+
eventCount: evidence.eventCount,
|
|
45
|
+
logLines: evidence.logLines,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 去重签名:结论 + 判据 + 重启次数(重启次数是唯一「单调量」,必须进签名)。 */
|
|
49
|
+
function sampleSignature(sample) {
|
|
50
|
+
return [sample.severity, sample.code, sample.findings.join(','), String(sample.restartCount)].join('|');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 这一条要不要追加。
|
|
54
|
+
*
|
|
55
|
+
* 只有「距上一条 < 30s **且** 签名完全相同」才丢——签名不同说明真发生了变化(容器刚被重启、
|
|
56
|
+
* 判据刚多了一条),哪怕只隔 1 秒也必须留下,否则趋势会把真正的拐点吃掉。
|
|
57
|
+
*/
|
|
58
|
+
export function shouldAppend(last, next, nowMs) {
|
|
59
|
+
if (last === undefined)
|
|
60
|
+
return true;
|
|
61
|
+
if (sampleSignature(last) !== sampleSignature(next))
|
|
62
|
+
return true;
|
|
63
|
+
const lastMs = Date.parse(last.at);
|
|
64
|
+
// 时间戳坏了(理论上不会:它由 sampleFromDiagnosis 生成)时按「可以追加」处理:
|
|
65
|
+
// 宁可多一条样本,也不要因为一条坏时间戳让整个历史从此停止记录
|
|
66
|
+
if (Number.isNaN(lastMs))
|
|
67
|
+
return true;
|
|
68
|
+
// 负数(时钟回拨)也算「窗口内」:那种情况下签名相同本来就没有新信息
|
|
69
|
+
return nowMs - lastMs >= HISTORY_DEDUPE_MS;
|
|
70
|
+
}
|
|
71
|
+
/** 取某个数值字段的首尾**非 null** 值;不足两个值返回 `null`(见文件头「三种没有」)。 */
|
|
72
|
+
function endsOf(samples, key) {
|
|
73
|
+
const values = [];
|
|
74
|
+
for (const sample of samples) {
|
|
75
|
+
const value = sample[key];
|
|
76
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
77
|
+
values.push(value);
|
|
78
|
+
}
|
|
79
|
+
if (values.length < 2)
|
|
80
|
+
return null;
|
|
81
|
+
return [values[0], values[values.length - 1]];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 每个数值项的**最小有意义变化**(低于它的抖动不进 `numbers`)。真机数据逼出来的:
|
|
85
|
+
* 第三次诊断的 `cpuPercent` 从 1.11 变到 1.09,`numbers` 里就多了一行
|
|
86
|
+
* 「CPU 1.11% → 1.09%(-0.02)」——那是瞬时快照的采样噪声,不是负载变化。
|
|
87
|
+
*
|
|
88
|
+
* 为什么是这些数:
|
|
89
|
+
* - `cpuPercent` **5 个百分点**:诊断只取一个 `docker stats --no-stream` 瞬时值,
|
|
90
|
+
* 1~2 个百分点的抖动是常态(真机实测 1.11 → 1.09 这种);5 个百分点以上才说明负载换了档。
|
|
91
|
+
* - `memPercent` **1 个百分点**:内存是慢变量,而且它直接对应 OOM 风险,小变化也要报。
|
|
92
|
+
* - `restartCount` **1**:这一项本来就不该动——重启多 1 次就是信号。
|
|
93
|
+
* - `errorLines` **3**:它数的是「最近 N 行里命中报错的行数」,而窗口是滑动的——
|
|
94
|
+
* 一条老报错滑出去、一条新的滑进来,计数就会 ±1~2。**真机实测**这台容器三连测是
|
|
95
|
+
* 22 → 21 → 21:差 1 行纯属窗口漂移,天天报出来会把真信号淹掉。3 行以上才算变化;
|
|
96
|
+
* 「从 0 变正」这种更重的信号由趋势判据 `error-growth` 单独负责,不靠这一行。
|
|
97
|
+
* - `eventCount` **5** / `logLines` **50**:这两项由采样窗口决定(日志尾行数、事件窗口),
|
|
98
|
+
* 抖动本来就大,太小的变化说出来只会淹掉真正的信号。
|
|
99
|
+
*/
|
|
100
|
+
const MIN_DELTA = {
|
|
101
|
+
restartCount: 1,
|
|
102
|
+
memPercent: 1,
|
|
103
|
+
cpuPercent: 5,
|
|
104
|
+
errorLines: 3,
|
|
105
|
+
eventCount: 5,
|
|
106
|
+
logLines: 50,
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* 归一:百分比类保留 1 位小数、计数类取整。
|
|
110
|
+
*
|
|
111
|
+
* 为什么必须做:`to - from` 在二进制浮点下会拖尾巴(`1.09 - 1.11 = -0.020000000000000018`、
|
|
112
|
+
* `88.1 - 52 = 36.099999999999994`),原样进 JSON 就是给客户端甩锅——无论它在哪一层四舍五入,
|
|
113
|
+
* 都要先跟这串数字搏斗一次。归一只在**出门前**做一次,样本里存的仍是原值(趋势判据要用真值)。
|
|
114
|
+
*/
|
|
115
|
+
function normalizeNumber(key, value) {
|
|
116
|
+
const factor = key === 'memPercent' || key === 'cpuPercent' ? 10 : 1;
|
|
117
|
+
return Math.round(value * factor) / factor;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 首尾真的变了的数值项。
|
|
121
|
+
*
|
|
122
|
+
* 两道闸:**归一后为 0**(没变)与**低于该 key 的最小有意义变化**(抖动)都不列。
|
|
123
|
+
* `delta` 由归一后的值相减、再归一一次——否则 `88.1 - 52` 这类减法仍会漏出 `36.099999999999994`。
|
|
124
|
+
*/
|
|
125
|
+
function changedNumbers(samples) {
|
|
126
|
+
const numbers = [];
|
|
127
|
+
// 键序即 `numbers` 的输出顺序(重启 > 内存 > CPU > 报错 > 事件 > 日志),保持稳定便于断言
|
|
128
|
+
for (const key of Object.keys(MIN_DELTA)) {
|
|
129
|
+
const ends = endsOf(samples, key);
|
|
130
|
+
if (ends === null)
|
|
131
|
+
continue;
|
|
132
|
+
const from = normalizeNumber(key, ends[0]);
|
|
133
|
+
const to = normalizeNumber(key, ends[1]);
|
|
134
|
+
const delta = normalizeNumber(key, to - from);
|
|
135
|
+
if (delta === 0)
|
|
136
|
+
continue;
|
|
137
|
+
if (Math.abs(delta) < MIN_DELTA[key])
|
|
138
|
+
continue;
|
|
139
|
+
numbers.push({ key, from, to, delta });
|
|
140
|
+
}
|
|
141
|
+
return numbers;
|
|
142
|
+
}
|
|
143
|
+
/** 固定格式的一位数百分比 / 计数(summary 里不要出现 `91.66667%` 这种)。 */
|
|
144
|
+
function fmt(value) {
|
|
145
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
146
|
+
}
|
|
147
|
+
/** 判据 code 列表 → 给人看的一句话(只取前三条,超出用「等」收尾)。 */
|
|
148
|
+
function joinFindings(codes) {
|
|
149
|
+
const head = codes.slice(0, 3).join('、');
|
|
150
|
+
return codes.length > 3 ? `${head} 等 ${String(codes.length)} 条` : head;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 样本序列 → 趋势。
|
|
154
|
+
*
|
|
155
|
+
* 判据按**优先级**取第一条命中的作为 verdict(`aggravating` 则是「任一命中」):
|
|
156
|
+
* 重启增长 > 反复 OOM > 内存爬升 > 报错增长 > 新出现严重判据 > 结论持续变严重 > 稳定。
|
|
157
|
+
* 顺序即重要性:一个正在 crash-loop 的容器,哪怕内存也在爬,第一句话该说「它在反复重启」。
|
|
158
|
+
*
|
|
159
|
+
* 文案是**给用户看的**(同 diagnose.ts):不说 `restartCount` / `memPercent` 这类字段名,
|
|
160
|
+
* 说「重启次数」「内存占用」;每条 ≤60 字。
|
|
161
|
+
*/
|
|
162
|
+
export function historyTrend(samples) {
|
|
163
|
+
const count = samples.length;
|
|
164
|
+
if (count < 2) {
|
|
165
|
+
return {
|
|
166
|
+
count,
|
|
167
|
+
window: null,
|
|
168
|
+
verdict: { severity: 'info', code: 'insufficient', summary: '诊断历史还不够形成趋势(至少 2 次)' },
|
|
169
|
+
numbers: [],
|
|
170
|
+
newFindings: [],
|
|
171
|
+
resolvedFindings: [],
|
|
172
|
+
aggravating: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const first = samples[0];
|
|
176
|
+
const last = samples[count - 1];
|
|
177
|
+
const firstFindings = new Set(first.findings);
|
|
178
|
+
const lastFindings = new Set(last.findings);
|
|
179
|
+
const newFindings = [...lastFindings].filter((code) => !firstFindings.has(code)).sort();
|
|
180
|
+
const resolvedFindings = [...firstFindings].filter((code) => !lastFindings.has(code)).sort();
|
|
181
|
+
const numbers = changedNumbers(samples);
|
|
182
|
+
const restartEnds = endsOf(samples, 'restartCount');
|
|
183
|
+
const restartGrowth = restartEnds === null ? null : restartEnds[1] - restartEnds[0];
|
|
184
|
+
const oomCount = samples.filter((sample) => sample.oomKilled).length;
|
|
185
|
+
const memEnds = endsOf(samples, 'memPercent');
|
|
186
|
+
const memGrowth = memEnds === null ? null : memEnds[1] - memEnds[0];
|
|
187
|
+
const errorEnds = endsOf(samples, 'errorLines');
|
|
188
|
+
const seriousNew = newFindings.filter((code) => SERIOUS_FINDINGS.has(code));
|
|
189
|
+
let verdict = null;
|
|
190
|
+
let aggravating = false;
|
|
191
|
+
// 1) 重启增长:±2 次说明这不是「偶发重启」,是 crash loop 的形状
|
|
192
|
+
if (restartGrowth !== null && restartGrowth >= 2) {
|
|
193
|
+
const from = restartEnds[0];
|
|
194
|
+
const to = restartEnds[1];
|
|
195
|
+
verdict = {
|
|
196
|
+
severity: restartGrowth >= 5 ? 'critical' : 'warning',
|
|
197
|
+
code: 'restart-growth',
|
|
198
|
+
summary: `重启次数在 ${String(count)} 次诊断里从 ${fmt(from)} 涨到 ${fmt(to)} —— 它在反复重启,不是偶发`,
|
|
199
|
+
};
|
|
200
|
+
aggravating = true;
|
|
201
|
+
}
|
|
202
|
+
// 2) 反复 OOM:出现两次以上,内存就是真的不够(不是抖动)
|
|
203
|
+
if (verdict === null && oomCount >= 2) {
|
|
204
|
+
verdict = {
|
|
205
|
+
severity: 'critical',
|
|
206
|
+
code: 'oom-recurring',
|
|
207
|
+
summary: `${String(count)} 次诊断里有 ${String(oomCount)} 次被 OOM 杀掉 —— 内存是真的不够`,
|
|
208
|
+
};
|
|
209
|
+
aggravating = true;
|
|
210
|
+
}
|
|
211
|
+
// 3) 内存爬升:涨了 20 个百分点**且**末值已经在 80 以上,才叫「爬升」(只要涨不看基线会误报)
|
|
212
|
+
if (verdict === null && count >= 3 && memGrowth !== null && memGrowth >= 20 && memEnds[1] >= 80) {
|
|
213
|
+
verdict = {
|
|
214
|
+
severity: 'warning',
|
|
215
|
+
code: 'memory-creep',
|
|
216
|
+
summary: `内存占用从 ${fmt(memEnds[0])}% 涨到 ${fmt(memEnds[1])}%(${String(count)} 次诊断)—— 有泄漏或流量在涨`,
|
|
217
|
+
};
|
|
218
|
+
aggravating = true;
|
|
219
|
+
}
|
|
220
|
+
// 4) 报错增长:涨了 5 行,或从 0 变正(「本来一条没有」到「开始报错」本身就是信号)
|
|
221
|
+
if (verdict === null && count >= 3 && errorEnds !== null) {
|
|
222
|
+
const [from, to] = errorEnds;
|
|
223
|
+
const grew = to - from >= 5 || (from === 0 && to > 0);
|
|
224
|
+
if (grew) {
|
|
225
|
+
verdict = {
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
code: 'error-growth',
|
|
228
|
+
summary: `日志里的报错行从 ${fmt(from)} 涨到 ${fmt(to)}(${String(count)} 次诊断)—— 错误在变多`,
|
|
229
|
+
};
|
|
230
|
+
aggravating = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// 5) 新出现的严重判据:OOM / crash loop / 不健康 / 非零退出,**新出现**就已经是恶化
|
|
234
|
+
if (verdict === null && seriousNew.length > 0) {
|
|
235
|
+
verdict = {
|
|
236
|
+
severity: 'warning',
|
|
237
|
+
code: 'new-serious-findings',
|
|
238
|
+
summary: `新出现严重判据:${joinFindings(seriousNew)}`,
|
|
239
|
+
};
|
|
240
|
+
aggravating = true;
|
|
241
|
+
}
|
|
242
|
+
// 6) 结论持续变严重:最近 3 条非降**且较窗口起点严格走高**,末条 ≥ warning
|
|
243
|
+
// (一次 warning 之后回到 ok 不算)。D169:此前只判「非降」,平手也满足单调,
|
|
244
|
+
// 于是连续 3 次都是 critical 的容器会被说成「在变严重」——严重度没变、findings 也没变,
|
|
245
|
+
// 那是「一直严重」,不是趋势。README 写的就是「连续走高」,判据以它为准。
|
|
246
|
+
if (verdict === null && count >= 3) {
|
|
247
|
+
const tail = samples.slice(-3);
|
|
248
|
+
const rankOf = (sample) => SEVERITY_RANK[sample.severity];
|
|
249
|
+
const monotone = tail.every((sample, index) => index === 0 || rankOf(sample) >= rankOf(tail[index - 1]));
|
|
250
|
+
const lastRisky = rankOf(tail[tail.length - 1]) >= SEVERITY_RANK.warning;
|
|
251
|
+
const rose = rankOf(tail[tail.length - 1]) > rankOf(tail[0]);
|
|
252
|
+
if (monotone && rose && lastRisky) {
|
|
253
|
+
verdict = {
|
|
254
|
+
severity: 'warning',
|
|
255
|
+
code: 'severity-creep',
|
|
256
|
+
summary: `连续 ${String(tail.length)} 次诊断的结论在变严重(最近一次:${last.severity})`,
|
|
257
|
+
};
|
|
258
|
+
aggravating = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
count,
|
|
263
|
+
window: { from: first.at, to: last.at },
|
|
264
|
+
verdict: verdict ?? { severity: 'ok', code: 'stable', summary: '容器状态没有明显变化' },
|
|
265
|
+
numbers,
|
|
266
|
+
newFindings,
|
|
267
|
+
resolvedFindings,
|
|
268
|
+
aggravating,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { Context } from '@deepseek-ai/cordis';
|
|
20
20
|
import z from '@deepseek-ai/schemastery';
|
|
21
21
|
import { DockerApi, assertBin, assertImageRef, parseImageHistoryJson, parseImageHistoryText, parseImageInspectJson, parseInspectJson, parsePsJson, parseStatsJson } from './docker.js';
|
|
22
|
+
import type { DiagnosisSample, HistoryTrend } from './history.js';
|
|
22
23
|
import type { DockerTarget, ResolvedTarget } from './docker.js';
|
|
23
24
|
import type { TierClass } from '@hyzyn/dsh-kit';
|
|
24
25
|
import type { HostKeyRecord, SshSpec } from './ssh-exec.js';
|
|
@@ -84,6 +85,18 @@ interface LocalProbeHint {
|
|
|
84
85
|
/** CLI 缺失时,PATH 里找到的别的容器 CLI(见 findAlternativeLocalCli),如 `podman`。 */
|
|
85
86
|
alternativeCli?: string;
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* 诊断历史视图(HTTP 与 agent 工具共用的一份形状):最近 N 条样本 + 趋势 + 本次是否真的记了一条。
|
|
90
|
+
*
|
|
91
|
+
* `samples` 里**没有日志原文**,这是刻意的(见 src/history.ts 文件头):历史只记结论与数字,
|
|
92
|
+
* 日志原文可能带凭证。客户端在历史块下方也会把这句话写给用户看。
|
|
93
|
+
*/
|
|
94
|
+
export interface DiagnosisHistoryView {
|
|
95
|
+
samples: DiagnosisSample[];
|
|
96
|
+
trend: HistoryTrend;
|
|
97
|
+
/** 本次调用是否真的追加了一条(去重命中时为 false)。 */
|
|
98
|
+
recorded: boolean;
|
|
99
|
+
}
|
|
87
100
|
/**
|
|
88
101
|
* SSE 帧封装:data 一律 `JSON.stringify` 成**单行**——换行 / 引号被转义,
|
|
89
102
|
* 多字节字符也不会被 SSE 的 `\n` 行边界截断(客户端 JSON.parse 还原)。
|