Skip to the content.

故障排查

先看日志

大多数问题的答案都在日志里,先确认看的是哪一份:

文件 什么时候看
logs/code-association.log 主日志(INFO 及以上),一次运行的完整时间线
logs/code-association-error.log 只看失败,不用在几万行 INFO 里翻
logs/console.log 服务根本没起来:logback 初始化前的报错、JVM 崩溃输出只在这里
logs/archive/*.log.gz 今天之前的(保留 7 天),用 zgrep 查

目录默认 ./logs,由 code-association.log-dir 决定(LOG_DIR 可覆盖)。 重要节点是 event=... 的结构化行,事件目录与现成的 grep 命令见 日志规范。 下文所有命令都假定在仓库根目录执行。

分析结果为空

按顺序检查:

1. 是否选了其他程序?

POST /api/analyses {"sourceId": 1, "peerSourceIds": []}
→ 400 {"error":"请至少选择一个要扫描的其他程序"}

只选被改动的程序本身不算——自身影响不在扫描范围内。

2. 本次是否真的有契约变更?

如果只改了内部实现(变量重命名、提取私有方法),没有触及任何契约, 那么对其他程序确实没有影响。看报告里的「变更清单」确认识别到了什么。

3. 对方是否真的在用这个契约?

看该 peer 条目的 matches:

{ "matches": [], "impacts": [], "note": null }

matches 为空说明没有找到使用点。常见原因:

对方程序读取失败

{ "peerSourceName": "order-service", "note": "无法读取该程序:目录不存在:/path" }

只有该条目失败,其他 peer 不受影响。检查位置是否可达、Git 凭据是否配置。

GitLab 私有仓库认证失败

现象:添加 GITLAB 代码源时分支下拉为空或提示认证失败;分析时克隆 / fetch 报 not authorized、Authentication failed 或 401。

按顺序检查:

检查项 说明与处理
来源类型 必须是 GITLAB。GITHUB / GIT / LOCAL_* 不会附加 GitLab 令牌,改成 GITLAB 后重试
令牌 scope 至少 read_repository;改过 scope 后要重新生成或刷新令牌
令牌本身 过期、被撤销,或复制时带上了空格 / 引号。在设置页重新粘贴保存(输入框是空的,占位符里的掩码不是明文)
环境变量遮蔽 代码源「凭据环境变量」指向的变量有值时会优先于设置页令牌;旧值会让设置页的令牌不生效。清空该字段或改对变量
自建实例地址 用 https://gitlab.example.com/group/repo.git 或 http://...;git@... 这类 SSH 地址不支持(这个 JGit 构建没有 SSH 传输),换成 HTTPS 克隆地址
令牌类型 个人访问令牌(PAT)。部署令牌(deploy token)需要用户名配对,本功能不适用
实际生效来源 看 GET /api/gitlab 的 configuredBy(file / application.yml)与 tokenSet
curl -s http://127.0.0.1:9996/api/gitlab
# {"tokenSet":true,"tokenMasked":"glpa…cdef","configuredBy":"file","settingsFile":"/.../gitlab-settings.json"}

排查时不要指望从日志里读到令牌:接口与日志都只有 set / none 与掩码。设置页保存的令牌对 同一 data-dir 上的 CLI 同样生效;流水线请用 GITLAB_TOKEN 环境变量或 --token-env (见 命令行与 CI)。

定时扫描不触发

确认配置生效

curl http://127.0.0.1:9996/api/schedule | python3 -m json.tool

enabled 必须是 true。注意 cron 是六段式(秒 分 时 日 月 周), 五段式会被解析成错误时间。

确认程序被正确跳过还是漏掉了

看 last.details:

outcome reason 含义
SCANNED 有新提交 abc123 → def456 正常
SKIPPED 没有新提交 正常
SKIPPED 已有分析在进行中 上一次还没跑完
SKIPPED 位置不可访问:/path 目录被删或移动
SKIPPED 没有其他程序可比较 只注册了一个程序
FAILED … 见对应错误

手动触发验证

curl -X POST http://127.0.0.1:9996/api/schedule/run
# 然后轮询
watch -n5 'curl -s http://127.0.0.1:9996/api/schedule | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[\"running\"], d[\"last\"][\"status\"])"'

接口很慢或超时

/api/analyses 慢

历史报告可能有几 MB(完整 JSON)。列表查询不应该读取报告内容。 如果这一项变慢,检查 Store.analyses() 是否用了 SELECT *——那会把所有 report_json 都读出来。

扫描一直 RUNNING

完整扫描要 fetch 远程仓库(每个可能几十秒)。4 个程序 2~3 分钟是正常的。 超过 10 分钟仍然 RUNNING,可能是网络卡住:

# 看是否卡在 fetch
tail -50 logs/code-association.log | grep -i fetch

# 或按阶段看耗时:准备(含 fetch)与差异识别各花了多久
grep 'event=analysis.prepare' logs/code-association.log | tail -5

强制恢复:重启服务,启动时会把孤儿任务标记为失败。

分析一直停在 96%

96% 只可能是 AI 评审段。 全仓库只有 AnalysisService 在进入 AI 段时写 96%(阶段 REPORT, detail「正在生成 AI 评审说明」);静态分析的各阶段在它之前就已经结束。

先确认卡在哪一段

curl -s 'http://127.0.0.1:9996/api/analyses?limit=1' | python3 -m json.tool | grep -A6 progress

progressStage 是 REPORT、detail 以 AI 评审 开头,就是 AI 段。刚进这一段时 detail 会先出现一次旧文案 「正在生成 AI 评审说明」(仍是 96%),随后才变成「AI 评审:…」——这是同一段的起始写入,不是漏了一行。 之后它在 96..99 之间按阶段推进,detail 依次是 AI 评审:生成评审要点、AI 评审:逐条评审意见、AI 评审:分块 i/n、 AI 评审:重定位意见 i/n、AI 评审:反思意见、AI 评审:整理意见(中间四行只在开启逐条评审时出现)。 一直不动的 detail 说明当前那次调用还没回来——用日志区分「慢」与「半死」:

grep 'event=analysis.ai' logs/code-association.log | tail -5
grep 'event=ai.review' logs/code-association.log | tail -5

常见原因

原因 表现
上游模型慢或「半死」 响应头回来了、body 不动;单次 HttpRequest.timeout 不覆盖 body 读取,以前会一直阻塞
开着逐条评审 comments: true 时一次分析最多 14 次串行模型调用(摘要 + ≤6 个分块 + ≤6 次重定位 + 1 次反思)
timeout-seconds 配得过大 单次上限大,整段就更容易长时间停在最后一个进度上

处置

  1. 等预算耗尽自愈。 预算到了会停止剩余调用,已生成的部分落进报告。reviewError 是否带预算句, 看预算有没有截断这一段,有两种形态:真的跳过了调用(budgetExpired=true);只缩短了最后一次调用的超时、 而那次调用失败(budgetExpired=false,评论缺失的原因仍是预算)。两种形态都拼成 「第一条具体错误;AI 评审超出时间预算(N 秒),已保留已生成的部分」,例如 第 1 个分块评审失败:模型调用超时(5 秒),请检查接口地址、网络或模型响应速度;AI 评审超出时间预算(8 秒),已保留已生成的部分; 没有具体错误时只有后半句,没被预算截断时保留原错误。逐段明细(剩余几个分块未评审、几条意见未重定位、 跳过反思)在 reviewStats.notes。budgetExpired 只表示真的跳过了调用,用于区分是否还有未送审的 分块,不等于评论是否完整;摘要本身失败时 report.ai.error 是 「AI 评审超出时间预算(N 秒),已保留已生成的部分」;分析随后照常完成。
  2. 调小总预算。 在 application.yml 设 code-association.ai.total-budget-seconds(重启生效); 不写时按 timeout-seconds × 3 推导并夹在 90..900 秒。
  3. 关掉逐条评审。 comments: false 把十几次调用降回一次摘要,最坏耗时从一刻钟降到一次请求。
  4. 换端点或模型。 用设置页「测试连接」确认 endpoint 与延迟,再重新生成。

静态分析结果不受影响,AI 只是可选的最后一段。 预算耗尽、模型报错、上游永久挂住,都不会改变 影响清单、路径与风险结论;最坏情况只是这次解释没有生成,可以从分析页重新生成。

分析在最后一步失败:serialize failed / TextBuffer overrun

现象:分析跑到最后(进度接近 100%、阶段是 REPORT)后变成失败,历史里只留下一条失败记录, 没有可查看的报告。logs/code-association-error.log 里是:

java.lang.IllegalStateException: serialize failed
  ... at com.geek.association.util.Json.write(Json.java:...)
  Caused by: com.fasterxml.jackson.core.exc.StreamConstraintsException:
  String length (...) exceeds the maximum length (...)

或者日志里出现 TextBuffer overrun。

原因:旧版本保存报告时把整份报告序列化成单个 String 再写进 report_json(SQLite TEXT)。 跨程序影响条目极多时(一次大变更产生数十万条并不罕见),这份 JSON 会超过 Jackson 文本缓冲的 2 GiB 上限,序列化直接抛错。失败发生在最后一步,所以前面的索引、传播、评分全部作废。

新版行为:报告里的影响清单按风险截断到 code-association.max-report-impacts 条 (默认 5000,名额按对比程序公平分配),其余条目只计数、不逐条写入 JSON:

字段 含义
report.impactsTotal 本次发现的影响总处数(全量)
report.impactsShown 清单里实际保留的条数
report.impactsTruncated 未列入清单的条数(0 表示没截断)
report.impactsNote 一句中文说明;未截断时为 null

影响审核页会在发现列表上方显示这句说明,页头的「N 项影响」用全量计数; summary 里的 impactedSymbols / highRisk / mediumRisk / lowRisk 也仍是全量计数。 截断只影响列表能逐条读多少,不改变风险结论。

处理:

  1. 升级到包含该修复的版本,重新运行一次分析。
  2. 确实需要逐条看更多影响时,在 application.yml 调大上限(重启生效):

    code-association:
      max-report-impacts: 20000
    

    上限越大,报告 JSON 越接近 2 GiB,落库失败的风险也越高——不要一路开到几十万。 命令行模式同样读取这个配置。

服务无响应但进程还在

典型症状:/api/health 正常,但静态资源请求挂住,logs/code-association.log(或 logs/code-association-error.log)里有 ClassNotFoundException: ch.qos.logback...。

原因:运行中的 JVM 打开了 target/code-association.jar,而这个 jar 被 mvn package 就地覆盖了。重启即可恢复。

预防:不要对着正在运行的实例执行 mvn package。构建到别的目录,或先停服务。

端口被占用

lsof -nP -iTCP:9996 -sTCP:LISTEN    # Linux 无 lsof 时:ss -ltnp | grep 9996
./stop.sh        # 会等到端口真正释放才退出
./start.sh

Windows:

netstat -ano | findstr :9996
stop.bat         :: 按 pid 文件 → 端口 → 命令行逐级兜底
start.bat

stop.sh / stop.ps1 都会等到端口真正释放才退出(最多 10 秒),避免紧接着启动撞端口。

两者的”停”并不相同:stop.sh 发 SIGTERM,JVM 会跑 shutdown hook;Windows 没有等价的信号, stop.ps1 只能结束进程(TerminateProcess),JVM 的 shutdown hook 不会执行。 这对本服务几乎没有影响:它没有注册任何 shutdown hook,SQLite 用 rollback journal 崩溃安全, 被中断的分析下次启动时会被标记为失败。

两处例外值得知道:

启停脚本报「缺少命令」或行为异常

start.sh / stop.sh 不硬依赖单一工具,但不同机器上走的分支不同:

症状 原因 处理
提示「没有 setsid 也没有 python3,退化为 nohup」 Linux 极简镜像 / macOS 无 python3 关掉终端可能带走服务;装 python3 或 util-linux(提供 setsid)可恢复
找不到监听 PID(PID 显示 ?) 没有 lsof / ss / netstat -tlnp 装任意一个即可;不影响启动本身,只影响 PID 文件准确性
健康检查一直超时但进程活着 没有 curl,走了 /dev/tcp 探测 装 curl;或直接用浏览器打开 http://127.0.0.1:9996/api/health
Windows 脚本中文乱码 / 语法报错 .ps1 被存成了无 BOM 的 UTF-8 用发布包里的原文件,不要用编辑器另存;release.sh 已做 BOM 校验
stop.bat 提示「进程无窗口,无法发送关闭信号」 旧版 stop.ps1 把 CloseMainWindow() 当主路径,而 java 是控制台进程,永远拿不到主窗口 升级即可:当前版本停止那一步只输出「已停止 (PID N)」,并省掉白等的 3 秒。旧包没有 -Verbose 输出,只能忽略这条提示——它不影响停止结果
stop.bat 说「已停止」但端口仍被占用 旧版没复核结束结果;或进程以管理员启动、当前 shell 权限不足 用管理员身份重开终端再执行 stop.bat;netstat -ano 查到 PID 后用任务管理器结束

深色模式下图表颜色不对

依赖图(Cytoscape)把颜色烘进画布,无法继承 CSS 变量。 主题切换时 app.js 会读取令牌计算值并重建图实例。如果颜色没更新,硬刷新一次页面。

AI 审核失败

AI 审核是可选层,它的失败不影响静态分析结论与 --fail-on 退出码。以下三种最常见。

401 / 403

现象:AI 卡片显示 HTTP 401: ... 或 HTTP 403: ...;「测试连接」返回 ok=false。

原因:

状态码 常见原因
401 Key 未填、已失效、被截断,或复制时带上了多余的空格与引号
403 Key 有效但无该模型的权限;组织策略或区域限制;余额 / 额度耗尽时部分服务也返回 403

处理:

  1. 在设置页「API Key」输入框重新粘贴一次完整 Key 并保存(输入框本身是空的,占位符里的 「已保存 sk-a…3f9c」才是掩码;留空保存表示不修改);
  2. 点「测试连接」,看返回的 endpoint 是不是预期的地址;
  3. 仍失败则用同一把 Key 直接 curl 一次,确认不是服务端账号问题:
curl -sS https://api.deepseek.com/v1/chat/completions \
  -H "Authorization: Bearer $AI_API_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'

日志里不会出现 Key,所以排查时不要指望从日志里读到它——能读到的只有 enabled / baseUrl / model / maxImpacts。

超时

现象:AI 卡片显示超时原因;「测试连接」返回 ok=false 且 message 含「连接超时」。

原因:

处理:

情况 处理
本地模型冷启动慢 先在终端跑一次让模型驻留;或在设置页把「请求超时(秒)」调大(上限 300,默认 60)
网络不可达 确认容器 / 服务能访问该端点;容器里的 localhost / 127.0.0.1 指容器自己,Ollama 在宿主机时要换地址
提示词太长 调小「每条分析最多送审影响」(默认 12)

超时只影响这一次解释,报告其它部分照常可用,可以稍后从分析页重试。

返回空内容

现象:AI 卡片显示「AI 评审说明生成失败」,原因是「模型返回了空内容」或「无法解析响应:…」; 「测试连接」却返回 ok=true。

原因:

处理:

  1. 用「测试连接」确认端点确实是 OpenAI 兼容的 /chat/completions——只实现了部分字段的 网关最容易出现这种”能连通但拿不到正文”;
  2. 换一个模型名再试(推理型模型与 deepseek-chat 这类对话模型的输出形态不同);
  3. 调小「每条分析最多送审影响」后重新生成,缩短提示词;
  4. 直接 curl 同一端点,看原始响应里 choices 与 message.content 到底是什么。

空内容被当成失败而不是成功:报告里记的是 generated: false 加原因,界面上是可重试的红色提示, 不会存下一张没有正文的卡片。「测试连接」只验证连通性(要求模型回 pong),所以它通过并不代表 完整提示词能拿到正文——两者用的不是同一个请求。

还想定位更细

# 分析进度(阶段 + 百分比 + 当前文件)
curl -s 'http://127.0.0.1:9996/api/analyses?limit=1' | python3 -m json.tool | grep -A3 progress

# 完整报告
curl -s 'http://127.0.0.1:9996/api/analyses/42/report' | python3 -m json.tool | head -80

报告里的 blindSpots 列出所有解析失败的调用点——”为什么没找到影响”的答案常在这里。

日志侧更细的查询(事件目录与字段含义见 日志规范):

# 一次分析的全部节点,按时间顺序
grep 'analysisId=42' logs/code-association.log

# 最近几次失败的阶段与原因
grep 'event=analysis.failed' logs/code-association.log | tail -10

# 最近几轮定时扫描:为什么有的程序没扫
grep 'event=scan.done' logs/code-association.log | tail -7

# 历史(今天之前,保留 7 天)
zgrep 'event=analysis.failed' logs/archive/*.log.gz