JulianoVinceCampos/postmortem-miner
GitHub: JulianoVinceCampos/postmortem-miner
将历史故障复盘报告自动转化为可解释的事件分流决策树和候选 SLI 的确定性分析工具。
Stars: 0 | Forks: 0
# postmortem-miner
[](https://github.com/JulianoVinceCampos/postmortem-miner/actions/workflows/pr-ci.yml)
[](https://sonarcloud.io/summary/new_code?id=JulianoVinceCampos_postmortem-miner)
[](https://sonarcloud.io/summary/new_code?id=JulianoVinceCampos_postmortem-miner)
[](https://scorecard.dev/viewer/?uri=github.com/JulianoVinceCampos/postmortem-miner)
[](LICENSE)
**[在线演示](https://postmortem-miner.onrender.com)** — 使用 `demo` / `demo` 登录。
基于合成语料库的只读公开实例。如果处于休眠状态,
可能需要几秒钟才能响应。
**你的 postmortem 已经知道是什么总是出错了。这个工具会将它们作为分流决策树重新读给你听。**
在项目附带的合成语料库中:
四个问题,用于将实时事件与档案中已有的所有记录进行比对分类。
剩下的 10% 是两个确实不属于任何模式的孤立事件(one-off)—— 工具会直接说明这一点,
而不是凭空捏造。
## 为什么
团队写了很好的 postmortem,但从不将它们作为一个整体来阅读。每一个都是
一个通宵的故事。如果一起阅读,二十个 postmortem 就会变成一张地图:同样
的四五种 failure modes,每一种都有独特的特征,大多数都有一个没人
有时间修复的 root cause。
这张地图正是你在事件发生第三分钟时所需要的东西,而手动构建它
会花费一个谁也抽不出的下午时间。所以:自动化阅读,保持推理过程的
可解释性(explainable),并让工具告诉你它什么时候不知道。
## 快速开始
```
git clone https://github.com/JulianoVinceCampos/postmortem-miner
cd postmortem-miner
make report # gera o corpus e reproduz o número acima
```
无需为此安装任何东西:该包仅使用标准库(standard library)
([ADR-0001](docs/adr/ADR-0001-zero-runtime-dependencies.md))。需要 Python 3.11+。
不使用 `make`:
```
python3 tools/gen_corpus.py --out corpus --count 18 --seed 7
PYTHONPATH=src python3 -m postmortem_miner.cli mine corpus --out out/report.md
```
针对你自己的档案:
```
python -m postmortem_miner.cli mine path/to/postmortems --out report.md --json analysis.json
```
在事件处理中途,使用你现在正在关注的信号:
```
python -m postmortem_miner.cli classify path/to/postmortems \
--signals saturation.pool.exhausted,store.lock.contention
# -> P2 pool 耗尽 + pool 等待
```
`postmortem-miner signals` 列出了它能识别的所有 token。
## Dashboard
Markdown 很好地回答了“这个集合说明了什么”。但在回答“那么这个事件呢,
现在?”时就表现得很糟。为此,提供了一个界面:
```
python -m postmortem_miner.cli serve corpus # http://127.0.0.1:8000
```
七个视图,按照问题出现的顺序排列:包含模式和信号层分布的概览,可导航的分流决策树,
**事件分类**(标记你看到的信号,获取可能的模式以及遍历的路径),
包含证据和 root cause 状态的详细模式,signal × pattern 矩阵,
包含提取的信号及其来源片段的事件目录,完整的分类法,以及报告 ——
报告完全就是 `mine` 的输出,而不是第二事实来源。
演示网关凭据:`demo` / `demo`,可通过 `PM_USER` 和
`PM_PASSWORD` 覆盖。验证在服务器端进行,使用 HMAC 签名的 cookie。这是基于只读合成数据的演示网关,
不是安全控制措施 —— 但仅在浏览器端验证的网关根本算不上是网关。
**仍然没有 runtime 依赖。** 服务器使用的是 `http.server`,session 使用的是 `hmac`,
前端没有框架也没有 CDN,图表是实时生成的 SVG。
[ADR-0003](docs/adr/ADR-0003-web-dashboard-on-stdlib.md) 重新审视了 ADR-0001 并解释了
为什么 FastAPI 被排除在外。
## Container 与部署
```
docker compose up --build # http://127.0.0.1:8000
```
镜像没有依赖解析步骤,因为根本没有需要解析的依赖。
以 non-root 用户身份运行,并带有请求 `/api/health` 的 `HEALTHCHECK`。
公开实例运行在 [postmortem-miner.onrender.com](https://postmortem-miner.onrender.com),由随仓库
提供的 Render blueprint(`render.yaml`)创建,并在 push 时自动部署。
端口没有在任何地方硬编码:平台注入 `PORT`,CLI 的默认值会从环境中读取。
在 `/app/corpus` 上挂载你自己的语料库来分析真实的 postmortem ——
该卷是只读的,工具永远不会写入语料库。
## 树长什么样
基于合成语料库生成,直接在报告中渲染:
```
flowchart TD
n0{"lifecycle.schedule.window?"}
n1{"app.cast_error?"}
n2["P5 cast error + npe
n=2"] n1 -->|yes| n2 n3{"app.retry_storm?"} n4["P6 retry storm + batch window
n=2"] n3 -->|yes| n4 n5["P8 deploy recent + query slow
n=2"] n3 -->|no| n5 n1 -->|no| n3 n0 -->|yes| n1 n6{"network.healthcheck.fail?"} n7{"lifecycle.cert.expired?"} n8["P4 cert expired + timeout external
n=2"] n7 -->|yes| n8 n9["P3 acl block + healthcheck fail
n=2"] n7 -->|no| n9 n6 -->|yes| n7 n10{"resource.cpu.saturated?"} n11["P2 pool exhausted + pool wait
n=3"] n10 -->|yes| n11 n12{"resource.gc.pressure?"} n13["P1 gc pressure + memory exhausted
n=3"] n12 -->|yes| n13 n14["P7 rollback long + transaction monolithic
n=4"] n12 -->|no| n14 n10 -->|no| n12 n6 -->|no| n10 n0 -->|no| n6 ``` 报告还包含一个 signal-by-pattern support matrix(信号与模式的支持度矩阵), 每次分类背后的 evidence snippet(证据片段),以及每种模式中有多少次发生的事件仍然存在未处理的 root cause 的统计。 最后一列通常是最让人不舒服的。 ## 工作原理 ``` markdown ──▶ Incident ──▶ signal tokens ──▶ patterns ──▶ decision tree ──▶ report parser signals patterns decision_tree report ``` **Parsing** 是刻意保持宽容的。Frontmatter 是可选的,接受英文和葡萄牙文的字段名, 并且日期会从 frontmatter、文件名或正文中提取。一个因为字段名而丢弃 postmortem 的 parser 是一个没人会去运行的 parser。 **信号提取** 通过精心整理的双语 regex 表将纯文本映射为标准的 token,例如 `saturation.pool.exhausted`:8 个层级中分布了 31 个 token, 每个 token 都带有生成它的文本 snippet。它们不是 embeddings, 并且 [ADR-0002](docs/adr/ADR-0002-regex-rules-not-embeddings.md) 长篇大论地解释了原因。简短的版本是: 在凌晨 3 点,你需要一个你可以争辩的结论,而不是一个你必须盲目相信的 similarity score。 **Clustering** 是基于信号集 Jaccard 相似度的 single-linkage 聚类。 K 值事先未知,一系列相关的事件应该能够连接在一起,而不会强迫生成一个 在操作上毫无意义的质心。 **独特信号** 是有趣的输出,而不是 clusters。一个出现在语料库所有事件中的信号是 background noise(背景噪音);一个在某个模式内频繁出现而在该模式外很罕见的信号则是 一个分流问题。margin(边际)要求是产生差异的关键。 **树** 是基于贪心算法(greedy)的 information gain,深度限制在 4。 更深的树得分更高,但帮不到任何人:没人会在生产环境宕机时去遍历九个问题。 一切都是确定性的。相同的语料库,输出端得到相同的字节 —— 这正是允许 CI 去维护这个 README 中的数字的原因,而不是寄希望于有人记得去更新它。 ## 信号分类法 | 层级 | 示例 Token | |---|---| | `resource` | `cpu.saturated`, `memory.exhausted`, `gc.pressure`, `disk.pressure` | | `saturation` | `pool.exhausted`, `pool.wait`, `threads`, `queue.backlog` | | `store` | `lock.contention`, `rollback.long`, `query.slow`, `transaction.monolithic` | | `network` | `acl.block`, `lb.imbalance`, `healthcheck.fail`, `timeout.external` | | `application` | `npe`, `cast_error`, `batch_error`, `retry_storm`, `error_swallowed`, `callback_missing` | | `lifecycle` | `deploy.recent`, `cert.expired`, `schedule.window`, `restart.reactive` | | `workload` | `traffic.spike`, `payload.large`, `batch.window` | | `topology` | `single_node`, `all_nodes` | 添加一条规则就是一行代码加上一个 fixture。参见 [CONTRIBUTING](CONTRIBUTING.md) —— 这是 你能做出的最有用的贡献。 ## 语料库是合成的,这是有意为之 项目附带的语料库由 `tools/gen_corpus.py` 生成:8 个事件系列加上 两个刻意拒绝形成 cluster 的孤立事件(one-off),一半为英文,一半为葡萄牙文, 对于给定的 seed 具有确定性。 这些 failure modes 是真实的,因为它们普遍存在于任何位于负载均衡器后面的 JVM 加上 关系型数据库的技术栈中。这里没有任何内容来自真实的系统、客户或同事。这是强制 执行的(enforced),而不仅仅是口头承诺:`tools/sanitize_scan.py` 会拦截 instance ids、account ids、tax ids、私有地址和内部主机名,并且它是 CI 中的 **第一个** 任务 —— 在 linting 之前 —— 因为在公开的 git 历史记录中的泄露是这里唯一无法撤销的错误。 测试套件保证了 gate 在没有任何 waiver 的情况下能够通过此仓库。 ## 目前尚不支持的功能 - **从事件历史中推导 SLI。** 令人兴奋的下一步: 该档案已经暗示了哪些指标本可以预测每种模式。计划在 0.2 版本中推出。 - **Feedback loop。** 对实时事件进行分类应允许将其附加到语料库中。 - **任何写入规模的操作。** Clustering 的复杂度是 O(n²);在数千个 postmortem 的规模下仍然可以接受。 ## 开发 ``` make install # extras de dev mais hooks de pre-commit make check # sanitize + lint + testes, na ordem do CI make cov # coverage mais o ratchet ``` 流水线运行十个层级:pre-commit、sanitize、lint、build 以及在三个 Python 版本上的测试, 带有只会上升的 ratchet 机制的 coverage,Semgrep,作为 blocking check 的 CodeQL,带有 OSV 的 dependency review,SonarCloud 的 quality gate,以及 SBOM 证明和 build provenance。 ## 文档 | 文档 | 内容 | | --- | --- | | [componentes.md](docs/componentes.md) | 组件、一张图表,以及每条边保证的内容 | | [ADR-0001](docs/adr/ADR-0001-zero-runtime-dependencies.md) | 为什么没有 runtime 依赖 | | [ADR-0002](docs/adr/ADR-0002-regex-rules-not-embeddings.md) | 为什么使用 regex 规则而不是 embeddings | | [ADR-0003](docs/adr/ADR-0003-web-dashboard-on-stdlib.md) | 为什么 dashboard 也是 stdlib | | [SECURITY.md](SECURITY.md) | 安全策略和防御层级 | | [CONTRIBUTING.md](CONTRIBUTING.md) | 如何贡献以及 CI 的要求 | ## 许可证 MIT。参见 [LICENSE](LICENSE)。
n=2"] n1 -->|yes| n2 n3{"app.retry_storm?"} n4["P6 retry storm + batch window
n=2"] n3 -->|yes| n4 n5["P8 deploy recent + query slow
n=2"] n3 -->|no| n5 n1 -->|no| n3 n0 -->|yes| n1 n6{"network.healthcheck.fail?"} n7{"lifecycle.cert.expired?"} n8["P4 cert expired + timeout external
n=2"] n7 -->|yes| n8 n9["P3 acl block + healthcheck fail
n=2"] n7 -->|no| n9 n6 -->|yes| n7 n10{"resource.cpu.saturated?"} n11["P2 pool exhausted + pool wait
n=3"] n10 -->|yes| n11 n12{"resource.gc.pressure?"} n13["P1 gc pressure + memory exhausted
n=3"] n12 -->|yes| n13 n14["P7 rollback long + transaction monolithic
n=4"] n12 -->|no| n14 n10 -->|no| n12 n6 -->|no| n10 n0 -->|no| n6 ``` 报告还包含一个 signal-by-pattern support matrix(信号与模式的支持度矩阵), 每次分类背后的 evidence snippet(证据片段),以及每种模式中有多少次发生的事件仍然存在未处理的 root cause 的统计。 最后一列通常是最让人不舒服的。 ## 工作原理 ``` markdown ──▶ Incident ──▶ signal tokens ──▶ patterns ──▶ decision tree ──▶ report parser signals patterns decision_tree report ``` **Parsing** 是刻意保持宽容的。Frontmatter 是可选的,接受英文和葡萄牙文的字段名, 并且日期会从 frontmatter、文件名或正文中提取。一个因为字段名而丢弃 postmortem 的 parser 是一个没人会去运行的 parser。 **信号提取** 通过精心整理的双语 regex 表将纯文本映射为标准的 token,例如 `saturation.pool.exhausted`:8 个层级中分布了 31 个 token, 每个 token 都带有生成它的文本 snippet。它们不是 embeddings, 并且 [ADR-0002](docs/adr/ADR-0002-regex-rules-not-embeddings.md) 长篇大论地解释了原因。简短的版本是: 在凌晨 3 点,你需要一个你可以争辩的结论,而不是一个你必须盲目相信的 similarity score。 **Clustering** 是基于信号集 Jaccard 相似度的 single-linkage 聚类。 K 值事先未知,一系列相关的事件应该能够连接在一起,而不会强迫生成一个 在操作上毫无意义的质心。 **独特信号** 是有趣的输出,而不是 clusters。一个出现在语料库所有事件中的信号是 background noise(背景噪音);一个在某个模式内频繁出现而在该模式外很罕见的信号则是 一个分流问题。margin(边际)要求是产生差异的关键。 **树** 是基于贪心算法(greedy)的 information gain,深度限制在 4。 更深的树得分更高,但帮不到任何人:没人会在生产环境宕机时去遍历九个问题。 一切都是确定性的。相同的语料库,输出端得到相同的字节 —— 这正是允许 CI 去维护这个 README 中的数字的原因,而不是寄希望于有人记得去更新它。 ## 信号分类法 | 层级 | 示例 Token | |---|---| | `resource` | `cpu.saturated`, `memory.exhausted`, `gc.pressure`, `disk.pressure` | | `saturation` | `pool.exhausted`, `pool.wait`, `threads`, `queue.backlog` | | `store` | `lock.contention`, `rollback.long`, `query.slow`, `transaction.monolithic` | | `network` | `acl.block`, `lb.imbalance`, `healthcheck.fail`, `timeout.external` | | `application` | `npe`, `cast_error`, `batch_error`, `retry_storm`, `error_swallowed`, `callback_missing` | | `lifecycle` | `deploy.recent`, `cert.expired`, `schedule.window`, `restart.reactive` | | `workload` | `traffic.spike`, `payload.large`, `batch.window` | | `topology` | `single_node`, `all_nodes` | 添加一条规则就是一行代码加上一个 fixture。参见 [CONTRIBUTING](CONTRIBUTING.md) —— 这是 你能做出的最有用的贡献。 ## 语料库是合成的,这是有意为之 项目附带的语料库由 `tools/gen_corpus.py` 生成:8 个事件系列加上 两个刻意拒绝形成 cluster 的孤立事件(one-off),一半为英文,一半为葡萄牙文, 对于给定的 seed 具有确定性。 这些 failure modes 是真实的,因为它们普遍存在于任何位于负载均衡器后面的 JVM 加上 关系型数据库的技术栈中。这里没有任何内容来自真实的系统、客户或同事。这是强制 执行的(enforced),而不仅仅是口头承诺:`tools/sanitize_scan.py` 会拦截 instance ids、account ids、tax ids、私有地址和内部主机名,并且它是 CI 中的 **第一个** 任务 —— 在 linting 之前 —— 因为在公开的 git 历史记录中的泄露是这里唯一无法撤销的错误。 测试套件保证了 gate 在没有任何 waiver 的情况下能够通过此仓库。 ## 目前尚不支持的功能 - **从事件历史中推导 SLI。** 令人兴奋的下一步: 该档案已经暗示了哪些指标本可以预测每种模式。计划在 0.2 版本中推出。 - **Feedback loop。** 对实时事件进行分类应允许将其附加到语料库中。 - **任何写入规模的操作。** Clustering 的复杂度是 O(n²);在数千个 postmortem 的规模下仍然可以接受。 ## 开发 ``` make install # extras de dev mais hooks de pre-commit make check # sanitize + lint + testes, na ordem do CI make cov # coverage mais o ratchet ``` 流水线运行十个层级:pre-commit、sanitize、lint、build 以及在三个 Python 版本上的测试, 带有只会上升的 ratchet 机制的 coverage,Semgrep,作为 blocking check 的 CodeQL,带有 OSV 的 dependency review,SonarCloud 的 quality gate,以及 SBOM 证明和 build provenance。 ## 文档 | 文档 | 内容 | | --- | --- | | [componentes.md](docs/componentes.md) | 组件、一张图表,以及每条边保证的内容 | | [ADR-0001](docs/adr/ADR-0001-zero-runtime-dependencies.md) | 为什么没有 runtime 依赖 | | [ADR-0002](docs/adr/ADR-0002-regex-rules-not-embeddings.md) | 为什么使用 regex 规则而不是 embeddings | | [ADR-0003](docs/adr/ADR-0003-web-dashboard-on-stdlib.md) | 为什么 dashboard 也是 stdlib | | [SECURITY.md](SECURITY.md) | 安全策略和防御层级 | | [CONTRIBUTING.md](CONTRIBUTING.md) | 如何贡献以及 CI 的要求 | ## 许可证 MIT。参见 [LICENSE](LICENSE)。
标签:SRE, 事故复盘, 代码示例, 偏差过滤, 决策树, 数据分析, 根因分析, 请求拦截, 逆向工具