Open Code Review 完全指南 — 会话持久化、遥测与查看器#
本章深入解析 Open Code Review(以下简称 OCR)的可观测性三件套——会话持久化、Manifest 可追溯性与遥测系统,以及配套的本地查看器。涵盖 JSONL 会话日志、Manifest 双边界状态机、会话恢复重放规则、评论查询、OpenTelemetry Span/Metric 体系、以及带 DNS rebinding 防护的嵌入式查看器。
1. 会话持久化(internal/session/)#
OCR 在每次审查运行时记录一份完整的会话日志(JSONL 格式),用于事后审计、断点恢复与查看器渲染。
1.1 存储路径与权限#
~/.opencodereview/sessions/<encodeRepoPath(repoDir)>/<sessionID>.jsonl
目录权限
0700:仅属主可读写执行,防止其他用户窥探会话内容。文件权限
0600:仅属主可读写,防止会话日志泄露代码与 LLM 交互。
sessionID 由 generateUUID 生成(UUID v4 实现)。每次运行一个独立的 .jsonl 文件,逐行追加,不覆盖。
1.2 encodeRepoPath 编码#
仓库路径不能直接作为目录名(含 /、\、: 等非法字符),需编码:
原字符 |
编码后 |
|---|---|
|
|
|
|
|
|
例如:
/home/user/my-project→-home-user-my-projectC:\Users\alice\repo→C_\-Users-alice-repo(\→-,:→_)
这一编码保证同一仓库的会话集中在一个目录下,且跨平台路径可安全地作为文件系统路径片段。
1.3 generateUUID v4 实现#
generateUUID 生成 RFC 4122 v4 UUID(随机版本)。v4 UUID 形如 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,其中 y 为 8/9/a/b。v4 保证时空唯一性,无需中心化协调,适合会话标识。
1.4 9 种记录类型#
JSONL 每行一个 JSON 对象,type 字段区分记录类型:
记录类型 |
写入时机 |
用途 |
|---|---|---|
|
会话开始 |
记录起始时间、options、repoDir |
|
文件审查完成 |
记录该文件的评论、rule、token 消耗 |
|
复用历史结果 |
缓存命中时记录复用 |
|
文件审查失败 |
记录错误,便于 resume 时跳过 |
|
LLM 请求发出 |
记录 prompt、model、参数 |
|
LLM 响应返回 |
记录响应内容与 usage |
|
LLM 调用出错 |
记录错误类型与消息 |
|
工具调用 |
记录工具名、参数、结果 |
|
会话结束 |
记录终止时间、终态 |
1.5 parentUuid 链式结构#
每条记录携带 parentUuid 字段,指向前一条记录的 UUID,形成链式结构。这使得会话日志可重建为树/链,便于:
追踪请求-响应配对(
llm_request→llm_response)还原 Agent 的工具调用序列(
tool_call间的父子关系)在查看器中渲染任务卡片的调用链
flowchart LR
SS["session_start<br/>uuid=A"] --> RID1["review_item_done<br/>uuid=B, parent=A"]
RID1 --> LR1["llm_request<br/>uuid=C, parent=B"]
LR1 --> LSP1["llm_response<br/>uuid=D, parent=C"]
LSP1 --> TC1["tool_call<br/>uuid=E, parent=D"]
TC1 --> RID2["review_item_done<br/>uuid=F, parent=A"]
RID2 --> SE["session_end<br/>uuid=G, parent=A"]
1.6 WriteLLMResponse 的 usage 字段#
llm_response 记录的 usage 字段是 token 计费与预算追踪的核心:
// internal/session/session.go
type LLMResponseRecord struct {
Type string `json:"type"` // "llm_response"
UUID string `json:"uuid"`
ParentUuid string `json:"parent_uuid"`
// ...
Usage Usage `json:"usage"`
}
type Usage struct {
PromptTokens int `json:"prompt_tokens"` // 输入 token
CompletionTokens int `json:"completion_tokens"` // 输出 token
CacheReadTokens int `json:"cache_read_tokens"` // 缓存命中读取
CacheWriteTokens int `json:"cache_write_tokens"` // 写入缓存
}
cache_read_tokens / cache_write_tokens 反映 prompt cache 的使用——缓存命中可大幅降低成本与延迟,是预算优化的关键观测点。
2. Manifest 系统(manifest.go)#
Manifest 是 OCR 可追溯性的核心:它以结构化、版本化的方式记录一次审查运行的输入、覆盖与结果,是"可复现审查"的凭证。
2.1 Schema 与操作#
// internal/manifest/manifest.go
const ManifestSchemaVersion = "ocr.run-manifest/v1"
v1 唯一支持的 Operation 是 OperationReview("review")。未来若有新操作(如 scan、explain),通过 schema 版本演进。
2.2 InputMode 三种#
InputMode |
含义 |
触发方式 |
|---|---|---|
|
审查一个 ref 范围 |
|
|
审查单个提交 |
|
|
审查工作区未提交变更 |
默认(无 ref 参数) |
2.3 FailureClass 八类#
文件级失败分类,记录在 review_item_failed:
FailureClass |
场景 |
|---|---|
|
LLM Provider 返回错误(鉴权、限流) |
|
请求超时 |
|
用户/CI 取消 |
|
配置错误(缺 endpoint、规则无效) |
|
输入问题(diff 解析失败) |
|
预算耗尽 |
|
OCR 内部 panic |
|
未知错误 |
2.4 RunFailureClass 七类#
运行级失败分类,记录在 Manifest 的 run 级摘要:
input / configuration / timeout / cancelled / budget / internal / unknown
RunFailureClass 与 FailureClass 类似但更粗粒度——它描述整个 run 为何未正常完成,而非单文件失败原因。
2.5 TerminalState 四态#
TerminalState |
含义 |
|---|---|
|
全部选定文件审查完成 |
|
部分完成(部分失败或被跳过) |
|
整体失败 |
|
整体被跳过(如无文件变更) |
2.6 ItemID 派生#
ItemID 是每个审查项的稳定标识,由输入参数哈希派生:
ItemID = SHA-256(operation + mode + oldPath + newPath)
operation:如reviewmode:InputMode(range/commit/workspace)oldPath/newPath:变更前/后路径
由于基于内容而非时间,相同输入产生相同 ItemID,是缓存复用与 resume 的关键。
2.7 Coverage 五集合#
Manifest 用五个集合描述覆盖情况:
graph TB
SEL["Selected<br/>= Completed ∪ Reused ∪ Failed ∪ Waived"]
COM["Completed"]
REU["Reused"]
FAL["Failed"]
WAI["Waived"]
SEL --- COM
SEL --- REU
SEL --- FAL
SEL --- WAI
style SEL fill:#e3f2fd
style COM fill:#c8e6c9
style REU fill:#fff9c4
style FAL fill:#ffcdd2
style WAI fill:#e0e0e0
关键恒等式:Selected = Completed ∪ Reused ∪ Failed ∪ Waived。这五集合互不相交(一个文件只属于其一),且并集等于选定集。这让覆盖审计一目了然——任何"消失"的文件都会破坏恒等式,立刻被发现。
2.8 ManifestBuilder 双边界#
ManifestBuilder 有两个不可逆的"封印"状态:
边界 |
触发方法 |
之后不可变内容 |
|---|---|---|
sealed |
|
选定文件集(不能再新增 selected) |
frozen |
|
全部结果(不能再写入 completed/failed/reused) |
flowchart LR
OPEN["开放<br/>可添加 selected"] -->|SealSelected| SEALED["sealed<br/>selected 锁定"]
SEALED -->|写入结果| WRITING["写入中<br/>completed/failed/reused"]
WRITING -->|Finalize| FROZEN["frozen<br/>全量锁定"]
FROZEN --> MANIFEST["输出 Manifest"]
style OPEN fill:#e8f5e9
style SEALED fill:#fff3e0
style WRITING fill:#fffde7
style FROZEN fill:#ffcdd2
style MANIFEST fill:#e3f2fd
双边界防止"事后篡改"——一旦选定集 sealed,不能再悄悄加入文件;一旦 Finalize frozen,不能再修改结果。这是可审计性的硬约束。
2.9 sanitizeReason 脱敏#
sanitizeReason 在写入 Manifest 前对 reason 文本脱敏,防止 secret 泄露到 Manifest。三类正则:
类别 |
正则目标 |
示例 |
|---|---|---|
URL 凭证 |
|
|
Bearer token |
|
→ |
secret 赋值 |
|
|
脱敏发生在 reason 写入前,保证 Manifest 可安全分享/归档。
3. 会话恢复(resume.go)#
OCR 支持从已有会话日志恢复审查,跳过已完成的文件,仅重跑失败或未完成的文件。
3.1 ResumeState 结构#
// internal/session/resume.go
type ResumeState struct {
SessionID string
Completed map[string]bool // 已完成的文件路径
Reused map[string]bool // 已复用的文件
Failed map[string]bool // 失败的文件(需重跑)
Options SessionOptions // 原会话的 options(用于校验)
}
3.2 LoadResumeState 重放规则#
LoadResumeState 重放 JSONL 日志,按记录类型更新状态:
记录类型 |
重放动作 |
|---|---|
|
提取 Options 用于校验 |
|
写入 |
|
写入 |
|
删除(从 Completed/Reused 移除,标记需重跑) |
review_item_failed 的"删除"语义很关键:失败的文件在 resume 时会被重跑,而非跳过。这保证 resume 不会"继承失败"。
3.3 ValidateOptions 校验#
resume 时必须校验当前 options 与原会话一致,否则拒绝:
flowchart TD
START["resume 请求"] --> CHK1{"InputMode == workspace?"}
CHK1 -->|是| REJ1["拒绝: workspace 模式不支持 resume<br/>(工作区内容易变化)"]
CHK1 -->|否| CHK2{"InputMode == range?"}
CHK2 -->|是| CHK3{"DiffFrom/DiffTo 与原会话匹配?"}
CHK3 -->|否| REJ2["拒绝: range 范围不一致"]
CHK3 -->|是| OK["通过校验,继续 resume"]
CHK2 -->|否 commit| CHK4["校验 commit 一致"]
CHK4 -->|否| REJ3["拒绝: commit 不一致"]
CHK4 -->|是| OK
style REJ1 fill:#ffcdd2
style REJ2 fill:#ffcdd2
style REJ3 fill:#ffcdd2
style OK fill:#c8e6c9
关键限制:workspace 模式不支持 resume。因为工作区内容随时变化,恢复点与当前状态可能不一致。range 模式要求 DiffFrom/DiffTo 与原会话完全匹配。
3.4 ValidateScanOptions 路径作用域校验#
若 resume 涉及 scan 选项,ValidateScanOptions 校验路径作用域(Paths)与原会话一致,避免"恢复时换了审查范围"导致覆盖集错乱。
5. 遥测系统(internal/telemetry/)#
OCR 集成 OpenTelemetry,提供 Span 与 Metric 两类可观测信号。遥测默认关闭,需显式启用。
5.1 启用与导出器#
遥测支持两种导出器:
导出器 |
用途 |
启用方式 |
|---|---|---|
|
调试,输出到 stdout |
|
|
集成,发送到 OTLP collector |
|
5.2 Span 树结构#
graph TB
RUN["review.run<br/>根 Span"]
RUN --> DIFF["diff.parse<br/>解析变更"]
RUN --> SUB1["subtask.execute.fileA<br/>文件 A 审查"]
RUN --> SUB2["subtask.execute.fileB<br/>文件 B 审查"]
RUN --> EV1["event.review.started"]
RUN --> EV2["event.no.files.changed"]
RUN --> EV3["event.plan.skipped"]
RUN --> EV4["event.plan.failed"]
RUN --> EV5["event.token.threshold.exceeded"]
RUN --> EV6["event.subtask.error"]
SUB1 --> EV7["event.* 子事件"]
style RUN fill:#e3f2fd
style SUB1 fill:#fff3e0
style SUB2 fill:#fff3e0
style EV1 fill:#f3e5f5
根 Span review.run 之下挂载 diff.parse 与各文件的 subtask.execute.<file> 子 Span,以及一系列 event.* 事件。事件以 Span 形式记录,便于在 trace 视图中按时间线查看。
5.3 事件列表#
事件 |
触发时机 |
|---|---|
|
审查开始 |
|
无文件变更 |
|
跳过 Plan 阶段 |
|
Plan 阶段失败 |
|
token 阈值超限 |
|
子任务出错 |
5.4 8 个 Metric#
Metric |
类型 |
含义 |
|---|---|---|
|
histogram |
审查总耗时 |
|
counter |
已审查文件数 |
|
counter |
生成评论数 |
|
counter |
LLM 请求总数 |
|
histogram |
LLM 请求耗时 |
|
histogram |
token 用量(含 cache 维度) |
|
counter |
工具调用总数 |
|
histogram |
工具执行耗时 |
5.5 配置优先级#
flowchart LR
DEF["defaults<br/>内置默认"] -->|被覆盖| CFG["config.json<br/>配置文件"]
CFG -->|被覆盖| ENV["env vars<br/>环境变量"]
ENV --> RESULT["生效配置"]
style ENV fill:#c8e6c9
style RESULT fill:#e3f2fd
优先级从低到高:defaults < config.json < env vars。环境变量优先级最高,适合 CI 中用 secret 覆盖。
5.6 环境变量#
环境变量 |
作用 |
|---|---|
|
启用遥测( |
|
OTLP collector 地址 |
|
协议( |
|
服务名(标识 OCR 实例) |
|
是否记录内容(prompt/response,默认关闭以保护隐私) |
5.7 OTLP 端口#
协议 |
默认端口 |
|---|---|
gRPC |
4317 |
HTTP |
4318 |
5.8 无采样配置#
OCR 遥测不配置采样,导出全部 Span 与 Metric。这与"可复现审查"理念一致——任何采样都可能丢失关键证据。代价是大数据量下需 collector 端做聚合/采样。
6. 查看器(internal/viewer/)#
OCR 内置一个嵌入式 HTTP 查看器,用于浏览会话日志与 Manifest,无需外部工具。
6.1 基本配置#
默认地址:
localhost:5483绑定:仅 localhost,不对外暴露
启动:
ocr viewer(或自动在 review 后启动)
6.2 DNS rebinding 防护#
查看器防御 DNS rebinding 攻击(恶意网站通过 DNS 重绑定访问 localhost 服务):
flowchart LR
REQ["HTTP 请求"] --> HG["hostGuard<br/>Host 头校验"]
HG --> WL{"Host 在白名单?"}
WL -->|是| OK["允许访问"]
WL -->|否| REJ["拒绝 403"]
style WL fill:#fff3e0
style OK fill:#c8e6c9
style REJ fill:#ffcdd2
hostGuard 校验 Host 头,仅允许白名单内的 host(如 localhost、127.0.0.1、[::1]),拒绝其他域名解析到本机的请求。这防止恶意网页通过 Host: evil.com(DNS 解析到 127.0.0.1)访问查看器读取会话内容。
6.3 三条 URL 路由#
URL |
页面 |
内容 |
|---|---|---|
|
仓库列表 |
所有有会话的仓库 |
|
会话列表 |
该仓库下所有 session |
|
会话详情 |
单次会话的任务卡片 |
6.4 5 种任务类型通道#
会话详情页按任务类型分通道展示:
任务类型 |
含义 |
|---|---|
|
Plan 阶段任务 |
|
Main Loop 主任务 |
|
审查过滤任务 |
|
内存压缩任务 |
|
重新定位任务 |
6.5 任务卡片结构#
每张任务卡片包含:
graph TB
CARD["任务卡片"]
CARD --> HEAD["Header"]
CARD --> BODY["Body"]
HEAD --> H1["请求号"]
HEAD --> H2["model"]
HEAD --> H3["token"]
HEAD --> H4["duration"]
HEAD --> H5["error 徽章(若有)"]
BODY --> RESP["Response<br/>LLM 响应内容"]
BODY --> TOOLS["Tool calls<br/>工具调用列表"]
style HEAD fill:#e3f2fd
style BODY fill:#fff3e0
Header 展示请求号、model、token 消耗、耗时,以及错误徽章(若失败)。Body 展示 LLM 响应内容与工具调用列表。
6.6 template FuncMap#
查看器使用 Go html/template,注册 FuncMap 提供格式化函数:
函数 |
作用 |
|---|---|
|
格式化耗时(如 |
|
格式化时间戳 |
|
截断长文本(如响应内容预览) |
|
数字千分位 |
|
加法(用于序号) |
|
卡片计数 |
|
任务类型 CSS class |
|
任务排序 |
6.7 orderedTasks 固定顺序#
orderedTasks 按固定顺序排列任务卡片,便于阅读:
PlanTask → MainTask → ReLocationTask → MemoryCompressionTask
这一顺序反映审查的执行流程:先 Plan,再 Main,必要时 ReLocation(重新定位评论位置),最后 MemoryCompression(压缩上下文)。
7. 配置示例#
7.1 启用 OTLP 导出#
~/.opencodereview/config.json:
{
"telemetry": {
"enabled": true,
"exporter": "otlp",
"otlp_endpoint": "http://localhost:4317",
"otlp_protocol": "grpc",
"service_name": "ocr-prod",
"content_logging": false
}
}
或用环境变量(CI 推荐):
export OCR_ENABLE_TELEMETRY=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_SERVICE_NAME=ocr-ci
export OCR_CONTENT_LOGGING=false
7.2 Jaeger 集成#
Jaeger 原生支持 OTLP,是最简单的 trace 后端:
flowchart LR
OCR["OCR<br/>otel SDK"] -->|OTLP gRPC:4317| COLL["Jaeger Collector<br/>内置 OTLP"]
COLL --> STORE["Jaeger Storage"]
STORE --> UI["Jaeger UI<br/>:16686"]
style OCR fill:#e3f2fd
style COLL fill:#fff3e0
style UI fill:#c8e6c9
启动 Jaeger(all-in-one,OTLP 接收):
docker run -d --name jaeger \
-p 4317:4317 \
-p 16686:16686 \
jaegertracing/all-in-one:latest
配置 OCR:
export OCR_ENABLE_TELEMETRY=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_SERVICE_NAME=ocr
随后在 http://localhost:16686 查看 review.run Span 树。
7.3 Datadog Agent 集成#
Datadog Agent 的 OTLP ingestion 端口同样为 4318(HTTP):
flowchart LR
OCR["OCR"] -->|OTLP HTTP:4318| DD["Datadog Agent"]
DD -->|forward| DDOG["Datadog 后端"]
DDOG --> DASH["Dashboards<br/>APM/Traces/Logs"]
style OCR fill:#e3f2fd
style DD fill:#732bbb
style DASH fill:#c8e6c9
Datadog Agent datadog.yaml 启用 OTLP:
otlp_config:
receiver:
protocols:
http:
endpoint: 0.0.0.0:4318
OCR 配置:
export OCR_ENABLE_TELEMETRY=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_SERVICE_NAME=ocr-prod
随后在 Datadog APM 中按 service:ocr-prod 过滤,查看 ocr.review.duration_seconds、ocr.llm.tokens_used 等指标与 trace。
8. 小结与设计哲学#
OCR 的可观测性三件套体现了"可审计性优先"的设计哲学:
会话日志全量持久化:JSONL 逐行追加,9 种记录类型 + parentUuid 链式结构,保证审查过程完整可重放。
Manifest 双边界状态机:sealed/frozen 两个不可逆封印,杜绝事后篡改;Coverage 五集合恒等式让覆盖审计一目了然。
resume 重放而非续跑:失败文件重跑,已完成文件跳过,且强制 options 一致校验,保证恢复点的语义正确。
遥测默认关闭、无采样:尊重隐私(content_logging 默认关闭),但一旦启用即全量导出,不丢失证据。
查看器 localhost-only + DNS rebinding 防护:便利与安全兼顾,hostGuard 白名单防止跨站访问。
这套可观测性体系是 OCR"确定性工程"的延伸——不仅审查结果确定,审查过程本身也可被完整观测、审计与复现。这为下一章的集成与扩展(CI/CD、Claude Code、Delegation Mode)提供了被信任的基础。
4. 评论查询(comments.go)#
LoadComments从会话日志加载评论,按文件完成顺序排列。4.1 加载逻辑#
后续覆盖前次:若同一文件在会话中出现多条
review_item_done(如重跑),后续的评论覆盖前次。这保证最终评论集反映最新结果,而非历史中间态。