bheus/guiltyspark
GitHub: bheus/guiltyspark
guiltyspark 是一个 AI 驱动的 Loki 日志监控 agent,能够自动检测异常、诊断根因,并借助 Codex 生成修复补丁和 GitHub PR,实现从日志告警到代码修复的闭环自动化。
Stars: 0 | Forks: 1
# guiltyspark
`guiltyspark` 是一个可部署的 agent,用于监控 Loki 日志、发现未知问题,并将有用的发现转化为可执行的报告。它旨在运行于任何常驻在线的 Docker 主机上。
其核心循环设计得非常简单:
1. 查询 Loki 获取最近的日志。
2. 将嘈杂的日志行分组归类为事件。
3. 调用 `codex exec` 来解释可能的 bug、配置错误和改进机会。
4. 将发现结果存储在本地,避免重复的噪音导致无休止的报警。
5. 将事件与其配置的 GitHub 仓库关联。
6. 在一个隔离的克隆环境中,让 Codex 准备最小修复和回归测试。
7. 强制执行补丁策略,运行验证,并选择性开启 PR。
## 它的功能
- 查看所有发送日志到 Loki 的应用。
- 搜索错误、重试、认证失败、服务降级、可疑的重启、缓慢的请求和反复出现的警告。
- 汇总证据,而不是直接转发原始的日志垃圾信息。
- 维护 SQLite 状态,用于游标记录和去重。
- 输出 JSONL 格式的发现结果,支持实时追踪、转发或接入通知系统。
- 使用通过 ChatGPT 认证的 Codex 会话,因此可以使用 ChatGPT Pro 额度来覆盖使用量,而无需单独计费的 OpenAI API key。
- 可在带有 Docker Compose 的树莓派上运行。
## 尚未实现的功能
- 除非在 `draft-pr` 或 `pr` 模式中明确配置了目标,否则它绝不会推送代码。
- 它不假定此 Codex 聊天能控制该树莓派。
- 除非你启用了修复/PR 工作流,否则它不需要本地仓库检入。
## 快速开始
```
cp .env.example .env
# 在 .env 或 Portainer 中设置 GUILTYSPARK_TARGETS_JSON。
docker compose pull
docker compose run --rm guiltyspark codex login --device-auth
docker compose up
```
要在本地构建容器而不是拉取 GHCR:
```
docker build -t guiltyspark:local .
GUILTYSPARK_IMAGE=guiltyspark:local docker compose up
```
进行本地试运行:
```
uv sync --dev --no-editable
cp .env.local.example .env.local
uv run --no-editable --reinstall-package guiltyspark guiltyspark once
```
在本地运行测试:
```
uv run --no-editable pytest
```
### 仪表板前端
仪表板 UI 是一个位于 `frontend/` 目录下的 React + TypeScript 应用。其构建
输出会存放在 `src/guiltyspark/web/` 中,Python 后端通过
`importlib.resources` 来提供这些文件。该目录是 **被 git 忽略的构建产物** —— Docker
镜像会在 Node 阶段构建它,并且 wheel 包会强制包含它。
```
# 日常 UI 工作:Vite dev server(将 /api 代理到运行在 :8343 上的 dashboard)
guiltyspark dashboard & # or run the daemon/compose service
npm --prefix frontend install
npm --prefix frontend run dev # open the printed http://localhost:5173
# 生成由 Python server 直接提供的 bundle
npm --prefix frontend run build
```
因为 `src/guiltyspark/web/` 没有被提交到仓库,在全新检出后直接运行原生的 `guiltyspark dashboard`
将无法提供任何内容,除非你运行过一次构建(或使用开发
服务器)。Docker 构建会自动处理此过程。
## 配置
所有设置均通过环境变量进行。最重要的变量如下:
| 变量 | 用途 |
| --- | --- |
| `LOKI_URL` | Loki 的 Base URL,例如 `http://loki:3100`。 |
| `LOKI_QUERY` | 要监控的 LogQL 查询语句,例如 `'{job=~".+"}'`。 |
| `LOKI_LIMIT` | 每次查询范围获取的最大日志行数。默认为 `5000`。 |
| `LOKI_BEARER_TOKEN` | 用于经过认证的 Loki 实例的可选 bearer token。 |
| `LOKI_BASIC_AUTH` | 用于 Loki 的可选 `user:password` 基础认证凭证。 |
| `CODEX_HOME` | 持久化的 Codex 认证/配置目录。在 Docker 中默认为 `/data/codex`。 |
| `GUILTYSPARK_INTERVAL_SECONDS` | daemon 模式下的轮询间隔。默认为 `300`。 |
| `GUILTYSPARK_LOOKBACK_SECONDS` | 如果不存在游标,初始的回溯时间。默认为 `900`。 |
| `GUILTYSPARK_MIN_EVENTS` | 一组日志行被算作一个事件所需的最小事件数。默认为 `2`。 |
| `GUILTYSPARK_MAX_INCIDENTS_PER_RUN` | 单个轮询周期内分析的事件数量上限。默认为 `8`。 |
| `GUILTYSPARK_STATE_PATH` | SQLite 状态路径。 |
| `GUILTYSPARK_FINDINGS_PATH` | JSONL 发现结果输出路径。 |
| `GUILTYSPARK_RUNBOOK_PATH` | agent 在分析前读取的 Markdown 运行手册。 |
| `GUILTYSPARK_NOTIFY_WEBHOOK_URL` | 用于推送新发现结果的可选通用 JSON webhook。 |
| `RESEND_API_KEY` | Resend API key。当 guiltyspark 开启一个 PR 时会发送一封电子邮件(仅对它自己开启的 PR 触发,绝不会干预你的手动 PR)。 |
| `GUILTYSPARK_NOTIFY_EMAIL_FROM` | 用于 PR 开启邮件且经过验证的 Resend 发件人地址。 |
| `GUILTYSPARK_NOTIFY_EMAIL_TO` | PR 开启邮件的收件人地址。 |
| `GUILTYSPARK_MODEL` | 传递给 `codex exec --model` 的模型。未设置则使用 Codex CLI 默认值。 |
| `GUILTYSPARK_CODEX_PATH` | Codex CLI 二进制文件路径。默认为 `codex`。 |
| `GUILTYSPARK_CODEX_WORKDIR` | Codex 可以检查的本地仓库/配置检出路径。 |
| `GUILTYSPARK_CODEX_TIMEOUT_SECONDS` | 单次 Codex 调用的超时时间。默认为 `600`。 |
| `GUILTYSPARK_PR_MODE` | `off`、`plan` 或 `branch`。脚手架默认为 `off`。 |
| `GUILTYSPARK_TARGETS_PATH` | 可选的 TOML 文件,用于将 Loki 查询映射到 GitHub 仓库。仅在首次运行时播种目标存储;此后以数据库为准。 |
| `GUILTYSPARK_TARGETS_JSON` | JSON 目标列表,用于 Portainer stack 配置。仅在首次运行时播种目标存储;之后可从仪表板编辑目标。 |
| `GUILTYSPARK_REMEDIATION_ROOT` | 用于存放短暂存在的隔离克隆的父目录。 |
| `GUILTYSPARK_GITHUB_TOKEN_ENV` | 包含 GitHub token 的环境变量名。 |
| `GUILTYSPARK_GITHUB_API_URL` | GitHub API base URL,用于 GitHub Enterprise。默认为 `https://api.github.com`。 |
| `GUILTYSPARK_DASHBOARD_HOST` | `guiltyspark dashboard` 的绑定地址。默认为 `0.0.0.0`。 |
| `GUILTYSPARK_DASHBOARD_PORT` | Web 仪表板端口。默认为 `8343`。 |
| `GUILTYSPARK_SITE_IMAGE` | 营销网站镜像。默认为 `ghcr.io/bheus/guiltyspark-site:latest`。 |
| `GUILTYSPARK_SITE_PORT` | nginx 营销网站容器的主机端口。默认为 `8080`。 |
| `GUILTYSPARK_DASHBOARD_GROUPING` | 启用后,仪表板会要求 Codex 将相关且未分配的异常聚类为一个单一的语义组,以便操作员可以一次性屏蔽整类异常,并提议一个**屏蔽模式**(一种服务作用域内的 regex),用于抑制当前*及未来*的变体。模式在生效前始终由操作员审查 —— UI 会显示该提议及其实时影响范围;绝不会自动应用。当出现新的异常类时会产生一次 Codex 调用开销(聚类基于未分配的指纹集进行缓存;仅数量变化会复用缓存),每次模式提议也会产生一次调用。需要 `codex` 二进制文件。如果发生任何 Codex 错误,则回退到平面列表。默认为 `false`。 |
| `GUILTYSPARK_DASHBOARD_FILTER_LABEL` | 仪表板的容器选择器过滤所依据的 Loki label。选择器会列出当前窗口该 label 的值,并在服务端缩小异常审查范围,从而使 `LOKI_LIMIT` 预算仅用于选定的容器。默认为 `container`。 |
| `GUILTYSPARK_DEDUP_ISSUES` | 启用时(默认),修复操作将基于*逻辑问题*而不是确切的指纹进行去重:Codex 将近乎重复的异常聚类为一个持久化的问题,每个问题每次运行仅修复一个代表性异常,并且如果某个问题的 PR 仍处于开启状态 —— 或者其上一个 PR 在冷却期内被合并/关闭 —— 则不会被重新提交。只有在出现真正的新指纹时才会产生 Codex 调用开销(已知指纹会直接从存储中进行短路处理);发生任何 Codex 错误时将回退到基于指纹的去重。需要 `codex` 二进制文件。默认为 `true`。 |
| `GUILTYSPARK_ISSUE_COOLDOWN_SECONDS` | 某个问题的上一个 PR(已合并或已关闭)之后,多长时间内抑制对其重新提交。无论此值如何,处于开启状态的 PR 始终会被抑制。默认为 `604800`(7 天)。 |
| `GUILTYSPARK_ISSUE_ACTIVE_WINDOW_SECONDS` | 在将新异常与现有问题匹配时,向后追溯已知问题的时长(用于界定聚类 prompt 的范围)。默认为 `1209600`(14 天)。 |
| `GUILTYSPARK_EXPECTED_LOGS_CACHE_SECONDS` | 每个目标获取的 `expected_logs_path` 文档在重新从 GitHub 获取前的缓存时间。默认为 `300`(5 分钟)。 |
| `GITHUB_APP_ID` | GitHub App ID。优先于个人 token 认证。 |
| `GITHUB_APP_INSTALLATION_ID` | 包含目标仓库的账户的 Installation ID。 |
| `GITHUB_APP_PRIVATE_KEY` | 作为字面量或 `\n` 转义 PEM 格式的 App 私钥。 |
| `GITHUB_APP_PRIVATE_KEY_FILE` | 挂载的 App 私钥 PEM 的替代路径。 |
## 仓库目标
Fleet 模式使用一个包含一个或多个 Loki 到仓库映射的 TOML 文件。从
[`targets.example.toml`](targets.example.toml) 开始:
```
[[targets]]
id = "inventory-service"
loki_url = "http://loki:3100"
loki_query = '''{container=~"inventory-(api|worker)"}'''
github_repo = "example-org/inventory-service"
base_branch = "main"
mode = "observe"
test_commands = ["pytest -q"]
allowed_paths = ["src", "tests"]
max_changed_files = 8
expected_logs_path = "docs/EXPECTED_LOGS.md"
```
`expected_logs_path`(可选)是相对于仓库的路径,指向一个文档,该文档列出了服务发出的
*预期*或良性的日志行 —— 故意的警告、启动
chatter、重试通知。Monitor 会从目标仓库(包括私有
仓库,使用相同的 GitHub 认证)获取该文件,并将其作为上下文传递给 Codex,从而避免将这些
模式误判为异常。如果文件丢失或无法访问,将被直接
忽略 —— 分析将在没有此额外上下文的情况下继续进行。获取结果会按
`GUILTYSPARK_EXPECTED_LOGS_CACHE_SECONDS` 进行缓存。
目标模式是逐步递进的:
- `observe`:仅检测和诊断。
- `fix`:克隆、编辑、强制执行策略并进行验证;绝不推送。
- `draft-pr`:执行相同的检查,然后推送一个 GuiltySpark 分支并开启一个 draft PR。
- `pr`:执行相同的检查,然后推送一个 GuiltySpark 分支并开启一个可进行审查的 PR。
生产环境可以通过 `GUILTYSPARK_TARGETS_JSON` 以 JSON 列表的形式提供相同的结构;这是首选的 Portainer 配置
方式,并且优先于本地的 TOML 文件。
目标存储在 SQLite 状态数据库中,并且可以在仪表板中进行编辑
(见下文)。`GUILTYSPARK_TARGETS_JSON` / `GUILTYSPARK_TARGETS_PATH` 仅在首次运行且
存储中不包含任何目标时**播种**该
存储;此后,数据库具有绝对权威,并且仪表板上的编辑将在重启后持久化保存。稍后更改环境变量不会
产生任何影响,除非存储为空,并且从
仪表板删除已播种的目标也不会因重启而恢复。daemon 会在
每个轮询周期开始时从存储中重新读取目标,因此编辑会在一个间隔内生效,而无需
重启。
对于私有仓库和 PR 模式
模式,推荐使用仅安装在配置的仓库上的 GitHub App。设置
`GITHUB_APP_ID`、`GITHUB_APP_INSTALLATION_ID` 以及 `GITHUB_APP_PRIVATE_KEY`
或 `GITHUB_APP_PRIVATE_KEY_FILE`。GuiltySpark 会生成并缓存短期的
installation token,用于控制器拥有的 Git 和 GitHub 请求;Codex 不会
接收 App 凭证或 installation token。当没有配置 App 变量时,通过
`GUILTYSPARK_GITHUB_TOKEN_ENV` 提供的 token 仍可作为后备使用。不完整的 App 配置被视为错误。
## 重放捕获的事件
重放固定数据(fixtures)使用与持久化修复
队列相同的通用事件和发现模式。要进行本地重放,请将目标示例复制到一个被忽略的配置文件中,将 `local_repo` 指向
相关的检出路径,并使用 `fix` 模式:
```
GUILTYSPARK_TARGETS_PATH=targets.local.toml guiltyspark replay \
tests/fixtures/example-upstream-outage.json \
--target inventory-service \
--patch-output data/example.patch
```
除非提供 `--allow-push` 参数,否则重放会将 `draft-pr` 和 `pr` 降级为 `fix`。这使得
相同的捕获事件既可以用于本地补丁评估,也可以用于明确授权的
draftPR 练习。
## 部署
Compose 堆栈还通过运行在 `http://localhost:8080` 的 nginx sidecar 提供静态营销网站
。设置 `GUILTYSPARK_SITE_PORT` 以在不同的
主机端口上发布它。仪表板依然在单独的端口 `8343` 上可用。主分支
的推送会在触发 Portainer 重新部署 webhook 之前,将 `site/` 发布为
`ghcr.io/bheus/guiltyspark-site:latest`,因此该堆栈不依赖于仓库的 bind mount。
向 `main` 分支推送约定式提交(conventional commits)。GitHub Actions 会对项目进行测试,创建一个语义化
发布,构建原生的 `linux/arm64` 镜像,将带有版本号和 `latest` 标签的镜像发布到 GHCR,
然后触发 GuiltySpark Portainer 堆栈 webhook,以便堆栈重新拉取 `latest`。
对于一次性的 Portainer 设置,请从此仓库的 `main` 分支创建堆栈,
在 Portainer 中启用堆栈 webhook,并将其生成的 URL 保存为 GitHub 仓库的
secret `PORTAINER_GUILTYSPARK_WEBHOOK`。发布工作流仅在新
镜像被推送之后才会调用该 webhook。如果缺少该 secret,则跳过 webhook 步骤,
而 Portainer 的常规 Git 轮询仍作为后备方案。
在部署时通过环境变量提供目标映射和凭证。
该仓库特意不包含特定于应用程序的目标配置。将每个
目标初始设置为 `observe` 模式,在审查诊断质量后将其升级为 `fix` 模式,并
仅在其验证命令和允许的路径确立后,才使用
`draft-pr` 或 `pr` 模式。
## 命令
```
guiltyspark once # poll Loki once and analyze current incidents
guiltyspark daemon # run forever
guiltyspark doctor # validate configuration and connectivity basics
guiltyspark dashboard # serve the web dashboard (default port 8343)
```
## 仪表板
`guiltyspark dashboard` 提供了一个带有 Monitor 语音风格的 Web 控制台(默认地址为
`http://localhost:8343`)。在 Docker 中,它作为 `guiltyspark-dashboard` compose
服务运行,该服务共享 daemon 的 `/data` 卷并发布 8343 端口。它展示
已编目的发现结果、修复历史,以及实时的
Loki 错误严重级别事件视图。每个实时事件都会被分类到
其流选择器匹配对应标签的目标中;任何与配置
目标不匹配的内容都将作为**未分配的异常**显示,因此 containment 协议之外的
错误仍然可见。容器选择器(由
`GUILTYSPARK_DASHBOARD_FILTER_LABEL` 支持)在服务端缩小了审查范围,因此
`LOKI_LIMIT` 预算仅会用于你选定的容器。
仪表板也是配置的控制平面:
- **Containment 协议** —— 添加、修改或停用目标。编辑内容将使用与配置文件相同的规则进行验证,
写入状态存储,并由 daemon 在其下一次循环中提取应用。
- **屏蔽噪音** —— 你认为属于噪音的未分配异常可以被屏蔽;它将
从流中剔除(以 incident 指纹为键)并列入
**已屏蔽的异常**列表中。屏蔽操作会捕获异常的服务、级别、一行样例
以及事件计数,使得该条目在离开数据流后依然保持清晰易读,并且每个条目
都带有一个可编辑的分流(triage)注释供你参考。任何条目都可以恢复。
- **屏蔽模式** —— 一种持久化、服务作用域内的 regex 规则,用于抑制噪音类别的当前
*及未来*变体,而不是一次只处理一个指纹。在启用
`GUILTYSPARK_DASHBOARD_GROUPING` 的情况下,Codex 会提议该模式,并且 UI 会显示
其实时影响范围;在你提交之前不会应用任何内容。规则包含可编辑的
标题和注释,无效的 regex 将被跳过,而不是导致异常视图崩溃。
在启用 `GUILTYSPARK_DASHBOARD_GROUPING` 的情况下,聚类运行在**请求路径之外**:
一个 worker 在后台计算组,当计算
正在进行时,API 会返回 `groups_pending`,并且 UI 会在扁平的回退列表上显示编目状态,每隔
2s 轮询一次,直到结果返回。
该页面是一个 React + TypeScript 应用(位于 `frontend/`,参见上文的开发说明),它
仅与 JSON API 通信,因此后端和客户端可以独立演进。它每 60s 轮询一次
并进行原地协调 —— 打开的事件卡片和正在进行的编辑
在刷新后依然保留。读取端点:`/api/overview`、`/api/findings`、
`/api/remediations`、`/api/anomalies?minutes=N[&containers=…]`、
`/api/containers?minutes=N`、`/api/targets`、`/api/anomalies/ignored`(被屏蔽的
指纹和模式规则)。写入端点:`POST`/`DELETE /api/targets`、
`POST`/`DELETE /api/anomalies/ignore`、`POST /api/anomalies/ignore-batch`(屏蔽一
整个组)、`POST /api/anomalies/suggest-pattern`(向 Codex 请求一个模式
提议)、`POST`/`DELETE /api/anomalies/rules`、`POST /api/anomalies/rules/metadata`
(编辑规则的标题/注释)以及 `POST /api/anomalies/note`(编辑被屏蔽异常的
triage 注释)。
仪表板是**未经过身份验证的**,并且它现在可以修改配置并触发
目标更改。请将其保留在受信任的局域网(LAN)中,不要将其暴露在公共互联网上。
## 认证说明
监控器使用 Codex CLI 而不是 `OPENAI_API_KEY`。在容器内使用你的 ChatGPT 账号登录一次:
```
docker compose run --rm guiltyspark codex login --device-auth
```
登录信息存储在 `CODEX_HOME` 下,并由 Compose 管理的
`guiltyspark-data` 卷提供支持。
标签:AIOps, Docker, Loki, 安全防御评估, 模块化设计, 自动化修复, 请求拦截, 运维监控, 逆向工具