sow log

Read the Operation audit ledger, export it as JSONL, and prune eligible terminal records.

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
StateMeaning
plannedCommand, arguments, targets and intended actions are durably recorded
stagedNew packages/metadata written to temporary locations and verified
appliedDesired state and required private pending payloads committed
builtThe complete static Generation has been switched in
doneTerminal — a normal successful command
done_dirtyTerminal — --skip was given, so the public tree deliberately stayed behind
failedTerminal — the operation failed before applied, nothing was committed
rolled_backTerminal — a post-applied failure the process could safely undo
recoveringNon-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"
}
FlagDescriptionDefault
-C, --workdir DIRWorkspace discovery start directorycurrent directory
-r, --repo NAMESelect a repositoryselection rules
-d, --dist NAMEShow only Operations touching this Distall
--jsonEmit the versioned JSON envelopefalse

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
FlagDescriptionDefault
-C, --workdir DIRWorkspace discovery start directorycurrent directory
-r, --repo NAMESelect a repositoryselection rules
-d, --dist NAMEExport only Operations touching this Distall

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.

FlagDescriptionDefault
-C, --workdir DIRWorkspace discovery start directorycurrent directory
-r, --repo NAMESelect a repositoryselection rules
-T, --timeout DURMaximum lock wait; 0 waits indefinitely0
-N, --no-waitFail immediately when the lock is heldfalse
--jsonEmit the versioned JSON envelopefalse

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

CommandCodeTrigger
log0Records printed, including an empty ledger
log2Usage error (including a non-numeric Operation ID), Workspace not found, or ambiguous selection
log5State database unreadable
log6The given Operation ID does not exist
log export0Export written
log export1I/O failure writing the target, or the parent is not a real directory
log export2Usage error or ambiguous selection
log export6Target already exists
log prune0Prune completed, including pruning nothing
log prune2Malformed BEFORE, -d given, or ambiguous selection
log prune4Repository lock held and --no-wait given or --timeout expired
log prune5Integrity or recovery error

See also

Last modified: 2026-08-08: init commit (fe725aa)