sow log

读取操作审计账本、导出为 JSONL,并清理符合条件的终态记录。

仓库内的每条写命令,都会在该仓库的 SQLite 中提交一条应用级 Operation,然后才产生任何外部文件 副作用。这条记录让崩溃恢复成为可能——而当 Operation 进入终态之后,同一条记录就是你的审计轨迹。 sow log 读的就是它。

语法

sow log [OPERATION] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME] [--json]
sow log export [FILE] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]
sow log prune BEFORE [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]

Operation 生命周期

读懂 state 字段,日志就读懂了一大半:

planned → staged → applied → built → done
                        └────────→ done_dirty
   any nonterminal → recovering → built / rolled_back
   pre-apply error  → failed
状态含义
planned命令、参数、目标与预期动作已持久化
staged新包/元数据已写入临时位置并校验通过
applied期望状态与所需的私有 pending 载荷已提交
built完整的静态 Generation 已切换
done终态——一次正常成功的命令
done_dirty终态——给了 --skip,公开树被有意保留在旧代
failed终态——在 applied 之前失败,什么都没提交
rolled_back终态——applied 之后失败,但进程安全地回滚了
recovering非终态;下一条写命令必须先完成或回滚它

工作区生命周期命令(initrepo newrepo rm)走的是工作区文件 journal,不会出现在仓库的 SQLite 日志中。dist new/dist rm 会出现——那时仓库数据库已经存在。

sow log

不带参数时,按由新到旧打印最近 50 条 Operation。

sow log -r pigsty

输出节选,operations 数组中的一个 Operation 对象:

{
  "id": "4262183287563704350",
  "kind": "build",
  "state": "done",
  "payload_json": "{\"version\":2,\"repository\":\"pigsty\",\"kind\":\"build\",\"config_sha256\":\"37eb6dcf...\",\"skip\":false,\"dists\":[\"el9\"],\"build_dists\":[\"el9\"],\"manifest_sha256\":\"678beeae...\"}",
  "result_json": "{\"dists\":1,\"dropped_pending\":[]}",
  "created_at": "2026-08-04T04:07:40.334787Z",
  "updated_at": "2026-08-04T04:07:40.907125Z"
}

payload_json 记录意图——包括当时生效配置的摘要 config_sha256,以及结果 Generation 的 manifest_sha256result_json 记录结果。失败的 Operation 还会带 error_classerror_message

{
  "id": "5995346754219751025",
  "kind": "add",
  "state": "failed",
  "result_json": "{\"accepted\":0,\"failed\":1}",
  "error_class": "rejected",
  "error_message": "no input package was accepted"
}
参数说明默认
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
-d, --dist NAME只显示触及该 Dist 的 Operation全部
--json输出版本化 JSON envelopefalse

查看单条 Operation

给出 Operation ID,就能得到它的完整状态迁移、耗时、包、成员与文件动作。

sow log 4262183287563704350 -r pigsty

输出节选:

{
  "duration_ms": 572,
  "events": [
    {"sequence": 0, "state": "planned",  "occurred_at": "2026-08-04T04:07:40.334787Z"},
    {"sequence": 1, "state": "staged",   "occurred_at": "2026-08-04T04:07:40.380963Z"},
    {"sequence": 2, "state": "applied",  "occurred_at": "2026-08-04T04:07:40.386186Z"},
    {"sequence": 3, "state": "built",    "occurred_at": "2026-08-04T04:07:40.904730Z"},
    {"sequence": 4, "state": "done",     "occurred_at": "2026-08-04T04:07:40.907125Z"}
  ],
  "packages": [],
  "memberships": [],
  "files": [
    {"sequence": 0, "action": "update", "phase": "pointer", "path": "dists/el9/aarch64/repodata/repomd.xml", "size": 1511, "sha256": "ef071821e06c9e86ab4f6d2a56906d82bb66df251e79d1086cfd44dc8395513e"},
    {"sequence": 1, "action": "update", "phase": "pointer", "path": "dists/el9/x86_64/repodata/repomd.xml",  "size": 1514, "sha256": "a31e90ec39169f0373b108458908333c96c5f600f3c63a50c44257856f0d2d55"}
  ]
}

files 数组使用与 sow changes 相同的 phase 词表:payloadmetadatapointerdelete

按 Dist 过滤

-d 把列表限制为触及该 Dist 的 Operation——一个仓库服务多个发行版时很有用:

sow log -d trixie -r pigsty

sow log export

把终态 Operation 以 JSONL 写出——每行一条完整的 Operation 明细记录——用于归档或送入日志管道。

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
exported 12 operations to /srv/audit/pigsty-ops.jsonl

省略 FILE 或传 - 则写到 stdout:

sow log export - -r pigsty | gzip > pigsty-ops-$(date +%F).jsonl.gz
参数说明默认
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
-d, --dist NAME只导出触及该 Dist 的 Operation全部

export 没有 --json——JSONL 就是它的输出格式。

它拒绝覆盖

目标已存在属于拒绝,绝不覆盖——审计导出不能静默毁掉上一份:

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
operation rejected: export target already exists: /srv/audit/pigsty-ops.jsonl

export 同样拒绝父目录不是真实目录的目标——符号链接,或根本不存在的目录:

sow log export /tmp/pigsty-ops.jsonl -r pigsty
log export parent is not a real directory

macOS 上 /tmp 是指向 /private/tmp 的符号链接,所以在那里会触发这条拒绝。请写到明确的真实路径。

sow log prune

删除早于 BEFORE 且符合条件的终态审计记录,并安全压缩数据库。

sow log prune 2027-01-01 -r pigsty
{"operation":"8150803833883584722","repository":"pigsty","before":"2027-01-01T00:00:00+08:00","pruned":1}

绝对时间戳会被回显,这样本地时区的解释永远不含糊。

参数说明默认
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
-T, --timeout DUR等待锁的最长时间;0 无限等待0
-N, --no-wait锁被占用时立即失败false
--json输出版本化 JSON envelopefalse

prune 在仓库级工作,不接受 -d——清掉半条 Operation 只会留下毫无意义的记录。

BEFORE 语法

BEFORE 是 ISO-8601 日期 YYYY-MM-DD(按本地时区零点解释),或带时区的 RFC 3339 时间戳。

sow log prune yesterday -r pigsty
usage error: BEFORE must be YYYY-MM-DD or an RFC 3339 timestamp with timezone

prune 永不删除什么

prune 在设计上是保守的。它绝不会删除:

  • 非终态的 Operation;
  • 当前恢复仍需要的记录;
  • 当前的 Package 或 Membership 状态;
  • Built Generation 或其 Changeset。

pruned 计数准确告诉你有多少条记录符合条件——通常少于截止时间之前的 Operation 总数。本版本中日志与 Changeset 位于同一个 SQLite 数据库,但保留规则不同。

示例

排查最近一次写入:

sow log -r pgsql --json | jq -r '.result.operations[0] | "\(.id)\t\(.kind)\t\(.state)"'

列出所有失败:

sow log -r pgsql --json | jq -r '.result.operations[] | select(.state=="failed") | "\(.id)\t\(.error_class)\t\(.error_message)"'

每月归档并收缩:

sow log export /srv/audit/pgsql-$(date +%Y%m).jsonl -r pgsql
sow log prune 2026-05-01 -r pgsql

哪条 Operation 最后触碰了某个 Dist:

sow log -d el9 -r pgsql --json | jq -r '.result.operations[0].id'

退出码

命令触发条件
log0记录已打印,包括空账本
log2用法错误(包括非数字的 Operation ID)、工作区未找到,或选择有歧义
log5状态数据库不可读
log6给定的 Operation ID 不存在
log export0导出成功
log export1写目标时 I/O 失败,或父目录不是真实目录
log export2用法错误或选择有歧义
log export6目标已存在
log prune0清理完成,包括一条都没清
log prune2BEFORE 格式非法、给了 -d,或选择有歧义
log prune4仓库锁被占用,且给了 --no-wait--timeout 到期
log prune5完整性或恢复错误

参见

最后修改:2026-08-08: init commit (fe725aa)