@hyzyn/dsh-docker 0.13.7 → 0.15.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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @hyzyn/dsh-docker — 容器「为什么不行」的判据(纯函数,零 IO)。
3
+ *
4
+ * 为什么单独一个文件:契约要求这里是**纯逻辑**——时钟、docker 命令、输出解析都由
5
+ * 调用方(`DockerApi.diagnose()`)采好再传进来。所以本文件不 import 任何 IO、
6
+ * 不读 `Date.now()`、不读 `process.env`:这些判据因此可以逐条「正例 + 反例」单测
7
+ * (见 `test/diagnose.test.ts`),而不是「跑一次真容器看看输出像不像」。
8
+ *
9
+ * **三种「没有」必须可区分**(本仓一贯的信号位原则,同 `truncated` / `limited` /
10
+ * `degraded` 那一套):
11
+ * - `null` —— **本次没采到**(采集失败)。evidence 对应字段给 `null`,且 findings 里
12
+ * 多一条 `*-unavailable`(info)说明原因;**绝不给 0**,否则「连不上」会被读成
13
+ * 「日志是空的 + 没有事件」,排障方向当场被带偏;
14
+ * - `''` / `[]` / `0` —— 采到了,但确实是空的(容器没有日志 / 这个窗口没有事件);
15
+ * - 可选字段缺席 —— 调用方没提供这项信息(内部胶水用,不对外承诺)。
16
+ *
17
+ * 文案是**给用户看的**:说「怎么了 + 怎么办」,不出现 `HostConfig` / `NanoCpus` /
18
+ * `docker inspect` 字段名这类实现术语(判据 8 说「内存上限 512MiB」而不是
19
+ * 「HostConfig.Memory=536870912」)。
20
+ */
21
+ import type { ContainerDetail, ContainerEvent, ContainerStats } from './docker.js';
22
+ /** 结论 / 单条判据的严重度。`ok` 只出现在 verdict 上(findings 只列「有问题」的)。 */
23
+ export type DiagnosisSeverity = 'critical' | 'warning' | 'info' | 'ok';
24
+ /** 单条判据的严重度(`ok` 不是问题,不进 findings)。 */
25
+ export type FindingSeverity = 'critical' | 'warning' | 'info';
26
+ export interface DiagnosisFinding {
27
+ code: string;
28
+ severity: FindingSeverity;
29
+ title: string;
30
+ detail: string;
31
+ }
32
+ export interface DiagnosisVerdict {
33
+ severity: DiagnosisSeverity;
34
+ /** 取最高 severity 的那条 finding 的 code;无 finding 时是 `running-ok` / `ok`。 */
35
+ code: string;
36
+ /** 一句话结论(≤60 字)。 */
37
+ summary: string;
38
+ }
39
+ /**
40
+ * 结构化证据(客户端据此渲染 `—`)。**所有可缺字段用 null,不省略**:客户端据此
41
+ * 区分「没有」与「没采到」。
42
+ */
43
+ export interface DiagnosisEvidence {
44
+ /** inspect 的 `State.Status`(`running` / `exited`)。 */
45
+ state: string;
46
+ /** ps 风格的 STATUS(`Up 4 hours (healthy)` / `Exited (137) 2 minutes ago`),见 `containerStatusText`。 */
47
+ status: string;
48
+ health: string | null;
49
+ /** 健康检查最后一次输出(原文;findings 的 detail 里会截断到 200 字)。 */
50
+ healthLastOutput: string | null;
51
+ exitCode: number | null;
52
+ oomKilled: boolean;
53
+ restartCount: number | null;
54
+ startedAt: string | null;
55
+ finishedAt: string | null;
56
+ /** 内存上限(字节);`null` = 没有设上限(docker 的 0 已归一成 null)。 */
57
+ memoryLimitBytes: number | null;
58
+ /** CPU 上限(纳核,docker inspect 的 NanoCpus);`null` = 没有设上限。 */
59
+ cpuQuota: number | null;
60
+ /** 本次抓到的日志行数;`null` = 日志采集失败。 */
61
+ logLines: number | null;
62
+ /** 本次抓到的日志里疑似报错的行数;`null` = 日志采集失败。 */
63
+ errorLines: number | null;
64
+ /** 事件窗口内的事件条数;`null` = 事件采集失败。 */
65
+ eventCount: number | null;
66
+ cpuPercent: number | null;
67
+ memBytes: number | null;
68
+ memPercent: number | null;
69
+ }
70
+ export interface DiagnosisTimelineEntry {
71
+ /** Unix 秒转 ISO 8601(`null` = 该事件没有时间)。 */
72
+ at: string | null;
73
+ /** 原样的事件动作(`ContainerEvent.action`,可能是 `health_status: healthy`)。 */
74
+ kind: string;
75
+ /** 给人看的中文一句话。 */
76
+ text: string;
77
+ }
78
+ export interface Diagnosis {
79
+ id: string;
80
+ name: string;
81
+ verdict: DiagnosisVerdict;
82
+ findings: DiagnosisFinding[];
83
+ evidence: DiagnosisEvidence;
84
+ /** 按时间**升序**(时间缺失的排最前)。 */
85
+ timeline: DiagnosisTimelineEntry[];
86
+ /** 最后 N 行原文(不带时间戳前缀);日志采集失败时为空串(findings 里有原因)。 */
87
+ logTail: string;
88
+ suggestions: string[];
89
+ }
90
+ /**
91
+ * 判据输入:**已采好的数据**,不是 api。
92
+ *
93
+ * 三路采集(logs / events / stats)的失败用 `null` 表示(见文件头「三种没有」),
94
+ * `*Error` 是给 `*-unavailable` 判据用的原因文案(可选:调用方没传就只说「采集失败」)。
95
+ */
96
+ export interface DiagnosisInput {
97
+ detail: ContainerDetail;
98
+ /** 内存上限(字节;0 = 不限制,调用方应已归一成 null)。 */
99
+ memoryLimitBytes: number | null;
100
+ /** CPU 上限(纳核;0 = 不限制,调用方应已归一成 null)。 */
101
+ cpuQuota: number | null;
102
+ /** 本次 `--tail` 采到的原文;`null` = 采集失败。 */
103
+ logText: string | null;
104
+ /** 采集到的日志行数;`null` = 采集失败(与「0 行」区分)。 */
105
+ logLines: number | null;
106
+ /** 窗口内、已按本容器服务端过滤的事件;`null` = 采集失败。 */
107
+ events: ContainerEvent[] | null;
108
+ /** 采集失败给 null(既有 ContainerStats)。 */
109
+ stats: ContainerStats | null;
110
+ logError?: string | null;
111
+ eventError?: string | null;
112
+ statsError?: string | null;
113
+ /**
114
+ * 「现在」的毫秒时间戳(只用于 `evidence.status` 里的相对时长,如 `Up 4 hours`)。
115
+ *
116
+ * 契约要求本模块**不读时钟**(时间由输入给),所以由调用方(`DockerApi.diagnose()`)
117
+ * 注入。缺席时不编时长,退化成不带时长的简写(`Up` / `Exited (137)`)。
118
+ */
119
+ nowMs?: number;
120
+ }
121
+ /**
122
+ * 相对时长的人类可读文案(`docker ps` 的 STATUS 列用)。
123
+ *
124
+ * 照着 docker 的 `units.HumanDuration` 分档复刻:**秒 → 分 → 时 → 天 → 周 → 月 → 年**,
125
+ * 其中 1 分钟 / 1 小时给 "About a minute" / "About an hour"(docker 就是这么念的),
126
+ * 「1 秒」单独成一档。刻意保留英文形态:用户从 `docker ps` 看到的是同一句话,
127
+ * 两处长得一样才不用在脑子里换算。
128
+ */
129
+ export declare function humanDuration(ms: number): string;
130
+ /**
131
+ * ps 风格的 STATUS 文案(`evidence.status` 用)。
132
+ *
133
+ * **为什么不直接用 `ContainerDetail.status`**:它实测就是 inspect 的 `State.Status`
134
+ * (`running` / `exited`),而 `docker ps` 的 STATUS 是另一列(`Up 4 hours (healthy)` /
135
+ * `Exited (137) 2 minutes ago`)——契约 §1.2 的 evidence 例子要的是后者。为了这一行
136
+ * 再跑一条 `docker ps` 不划算(诊断已经有 inspect 的权威字段),所以按 docker ps 的
137
+ * 口径现算:判据用 inspect,展示文案顺手推出来(一条事实一个来源)。
138
+ *
139
+ * `nowMs` 缺席时不给相对时长(不编「多久以前」),退化成 `Up` / `Exited (137)`。
140
+ */
141
+ export declare function containerStatusText(detail: ContainerDetail, nowMs?: number): string;
142
+ /**
143
+ * 纯函数入口:把已采好的四路数据判成一份诊断。
144
+ *
145
+ * 顺序稳定:findings 按 severity 降序,同级按上表的编号顺序;verdict 取最高 severity
146
+ * 的那条(同级取最靠前),一条都不命中且容器在运行时给 `running-ok`。
147
+ */
148
+ export declare function diagnose(input: DiagnosisInput): Diagnosis;