sow log
Every write command inside a Repository commits an application-level Operation to that Repository’s
SQLite database before it produces any external file side effect. That record is what makes crash
recovery possible — and once the Operation reaches a terminal state, the same record is your audit
trail. sow log reads it.
Synopsis
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 lifecycle
Understanding the state field is most of understanding the log:
planned → staged → applied → built → done
└────────→ done_dirty
any nonterminal → recovering → built / rolled_back
pre-apply error → failed
| State | Meaning |
|---|---|
planned | Command, arguments, targets and intended actions are durably recorded |
staged | New packages/metadata written to temporary locations and verified |
applied | Desired state and required private pending payloads committed |
built | The complete static Generation has been switched in |
done | Terminal — a normal successful command |
done_dirty | Terminal — --skip was given, so the public tree deliberately stayed behind |
failed | Terminal — the operation failed before applied, nothing was committed |
rolled_back | Terminal — a post-applied failure the process could safely undo |
recovering | Non-terminal; the next write command must complete or roll it back |
Workspace lifecycle commands (init, repo new, repo rm) use the Workspace file journal instead
and do not appear in a Repository’s SQLite log. dist new/dist rm do appear — the Repository
database already exists at that point.
sow log
With no argument, prints the 50 most recent Operations, newest first.
sow log -r pigsty
Output excerpt, one Operation object from the operations array:
{
"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 records the intent — including config_sha256, the digest of the configuration in
force, and manifest_sha256 for the resulting Generation. result_json records the outcome. A
failed Operation additionally carries error_class and error_message:
{
"id": "5995346754219751025",
"kind": "add",
"state": "failed",
"result_json": "{\"accepted\":0,\"failed\":1}",
"error_class": "rejected",
"error_message": "no input package was accepted"
}
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR | Workspace discovery start directory | current directory |
-r, --repo NAME | Select a repository | selection rules |
-d, --dist NAME | Show only Operations touching this Dist | all |
--json | Emit the versioned JSON envelope | false |
One Operation in detail
Pass an Operation ID to get its full state transitions, timing, packages, memberships and file actions.
sow log 4262183287563704350 -r pigsty
Output excerpt:
{
"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"}
]
}
The files array uses the same phase vocabulary as
sow changes: payload, metadata, pointer, delete.
Filtering by Dist
-d restricts the listing to Operations that touched that Dist — useful when one Repository serves
several distributions:
sow log -d trixie -r pigsty
sow log export
Writes terminal Operations as JSONL — one complete Operation detail record per line — for archival or ingestion into a log pipeline.
sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
exported 12 operations to /srv/audit/pigsty-ops.jsonl
Omit FILE, or pass -, to write to stdout:
sow log export - -r pigsty | gzip > pigsty-ops-$(date +%F).jsonl.gz
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR | Workspace discovery start directory | current directory |
-r, --repo NAME | Select a repository | selection rules |
-d, --dist NAME | Export only Operations touching this Dist | all |
export has no --json; JSONL is its output format.
It refuses to clobber
An existing target is a rejection, never an overwrite — an audit export must not silently destroy a previous one:
sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
operation rejected: export target already exists: /srv/audit/pigsty-ops.jsonl
export also refuses a target whose parent is not a real directory — a symlink, or a directory that
does not exist:
sow log export /tmp/pigsty-ops.jsonl -r pigsty
log export parent is not a real directory
On macOS /tmp is a symlink to /private/tmp, so that refusal fires there. Write to an explicit
real path instead.
sow log prune
Deletes eligible terminal audit records older than BEFORE and safely compacts the database.
sow log prune 2027-01-01 -r pigsty
{"operation":"8150803833883584722","repository":"pigsty","before":"2027-01-01T00:00:00+08:00","pruned":1}
The absolute timestamp is echoed back so the local-timezone interpretation is never ambiguous.
| Flag | Description | Default |
|---|---|---|
-C, --workdir DIR | Workspace discovery start directory | current directory |
-r, --repo NAME | Select a repository | selection rules |
-T, --timeout DUR | Maximum lock wait; 0 waits indefinitely | 0 |
-N, --no-wait | Fail immediately when the lock is held | false |
--json | Emit the versioned JSON envelope | false |
prune operates at Repository level and does not accept -d — pruning half an Operation would
produce a meaningless record.
BEFORE syntax
BEFORE is an ISO-8601 date YYYY-MM-DD, interpreted as local midnight, or an RFC 3339 timestamp
with a timezone.
sow log prune yesterday -r pigsty
usage error: BEFORE must be YYYY-MM-DD or an RFC 3339 timestamp with timezone
What prune never deletes
prune is conservative by construction. It never removes:
- a non-terminal Operation;
- a record still required for recovery;
- current Package or Membership state;
- a Built Generation or its Changeset.
The pruned counter tells you exactly how many records were eligible, which is normally fewer than
the number of Operations older than the cutoff. Log and Changeset live in the same SQLite database in
this release, but they follow different retention rules.
Examples
Investigate the most recent write:
sow log -r pgsql --json | jq -r '.result.operations[0] | "\(.id)\t\(.kind)\t\(.state)"'
List everything that failed:
sow log -r pgsql --json | jq -r '.result.operations[] | select(.state=="failed") | "\(.id)\t\(.error_class)\t\(.error_message)"'
Archive and shrink, monthly:
sow log export /srv/audit/pgsql-$(date +%Y%m).jsonl -r pgsql
sow log prune 2026-05-01 -r pgsql
Which Operation last touched a Dist:
sow log -d el9 -r pgsql --json | jq -r '.result.operations[0].id'
Exit codes
| Command | Code | Trigger |
|---|---|---|
log | 0 | Records printed, including an empty ledger |
log | 2 | Usage error (including a non-numeric Operation ID), Workspace not found, or ambiguous selection |
log | 5 | State database unreadable |
log | 6 | The given Operation ID does not exist |
log export | 0 | Export written |
log export | 1 | I/O failure writing the target, or the parent is not a real directory |
log export | 2 | Usage error or ambiguous selection |
log export | 6 | Target already exists |
log prune | 0 | Prune completed, including pruning nothing |
log prune | 2 | Malformed BEFORE, -d given, or ambiguous selection |
log prune | 4 | Repository lock held and --no-wait given or --timeout expired |
log prune | 5 | Integrity or recovery error |
See also
- Observability & Audit — how
logfits withstatus,checkandchanges - Transactions & Recovery — the journal that the log records
- sow changes — the physical counterpart to a semantic Operation
- JSON Output — the full log result schema