Skip to content

feat: record durable run history in a ClickHouse table - #7

Merged
lesandie merged 1 commit into
masterfrom
feat/durable-run-log
Aug 24, 2026
Merged

feat: record durable run history in a ClickHouse table#7
lesandie merged 1 commit into
masterfrom
feat/durable-run-log

Conversation

@lesandie

@lesandie lesandie commented Aug 21, 2026

Copy link
Copy Markdown
Member

Summary

Adds a durable ClickHouse run log for collect, dry-run, and delete events. Log writes use a separate client, failures degrade safely to stdout, and messages are redacted.

Validation

  • CI: passed
  • Local: 112 tests passed; rendered manifest validates
  • Live dev run: run log recorded collect, dry-run, checkpoints, and verification

Stacked on #6; followed by #9 and #10.

@lesandie

Copy link
Copy Markdown
Member Author

Review record

Reviewed alongside #6 and #9.

Verified: deletion scope untouched — #6's eleven scope mutations re-run against this branch, 11/11 caught. pytest 93 passed / 1 xfailed, render + kubeconform clean.

Three constraints checked by test, since this is bookkeeping attached to an irreversible operation:

Constraint Test
Writes on ch_writer, never ch_client (the SESSION_IS_LOCKED 373 trap from 0.6.0, which here would fire mid-delete) test_run_log_writes_off_the_streaming_session
A logging failure never fails the run; disables after one failure rather than retrying per batch test_a_failed_run_log_write_never_fails_the_run
Missing CREATE TABLE grant degrades to stdout, does not abort test_missing_create_grant_degrades_to_stdout_only
Credentials redacted before insert test_run_log_redacts_secrets

Sink choice, for the record: not a PVC (readOnlyRootFilesystem, and emptyDir dies with the pod); not the bucket (needs s3:PutObject, and an object under S3PATH would be listed by the next collect and become a deletion candidate — s3gc would GC its own logs).

Open question for reviewers: RUNLOG defaults to true, so existing env files start creating a second table on the target cluster. Deliberate — the CREATE TABLE grant is already required for the aux table — but it is a new side effect from an unchanged config.

@lesandie

Copy link
Copy Markdown
Member Author

Open question resolved: RUNLOG stays default-on.

Rationale: the table is queryable in ClickHouse for exactly the cases where pod logs are gone, which is the point of the change. The CREATE TABLE grant is already required for the auxiliary table, so it costs no new permission, and a missing grant degrades to stdout rather than failing the run.

No code change — the branch already defaults to on. RUNLOG=false remains available per-deployment.

@lesandie lesandie self-assigned this Aug 22, 2026
Base automatically changed from test/pin-deletion-scope to master August 24, 2026 14:39
Pod logs are not a record. The kubelet rotates container output (10Mi over 5
files by default), so `kubectl logs` cannot return the beginning of a long
run, and ttlSecondsAfterFinished deletes the Job and its pods along with
everything they printed. A cleanup that reclaimed terabytes left no evidence
of what it did once that window closed.

Each run now appends structured events to <COLLECTTABLEPREFIX><disk>_log,
beside the auxiliary table and NEVER truncated: phase start, throttled collect
progress, per-sample start, one row per confirmed delete batch, finish with
attempt totals, warnings and errors. Rows carry the scope the run was pointed
at -- bucket, prefix, disk, cluster, dry-run, ClickHouse host, hostname -- so a
row is self-describing evidence rather than a line of text. S3GC_RUNID defaults
to a generated timestamped id and the Job template passes JOB_NAME, so a row
traces back to the Job that wrote it.

ClickHouse is the sink rather than a volume or the bucket: the connection,
credentials and grants already exist; a readOnlyRootFilesystem container cannot
write a file and an emptyDir dies with the pod; and an object written under
S3PATH would be listed by the NEXT collect, found absent from
system.remote_data_paths, and become a deletion candidate -- s3gc would
garbage-collect its own logs.

Three constraints, each with a test, because this is bookkeeping attached to an
irreversible operation:

  1. Writes go on ch_writer, never ch_client. do_use() holds ch_client's
     session for the whole anti-join stream, and a second query on a held
     session is SESSION_IS_LOCKED (373) -- the 0.6.0 defect, which would now
     fire mid-delete at the worst possible moment.
  2. A logging failure never fails the run. One failure disables the run log
     for the remainder of the process rather than retrying every batch, and a
     missing CREATE TABLE grant degrades to stdout only with one warning.
  3. Messages are redacted through the existing LogFormatter._filter before
     insert, so a credential cannot reach a table that outlives the run.

Collect also reports progress at INFO, throttled to every 100k objects. Per-
batch progress was DEBUG-only, so a multi-hour collect emitted about four lines
at --verbose, while --debug emits one line per object and on a large bucket
exceeds the kubelet's rotation limit, destroying the start of its own output.

Opt out with --runlog false / S3GC_RUNLOG_FLAG=false, or RUNLOG=false in the
renderer. Default on: durability is the point. Legacy env files render
unchanged with the run log enabled.

The deletion scope is untouched. All 11 scope mutations from the parent commit
remain caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lesandie
lesandie force-pushed the feat/durable-run-log branch from a7f3ad2 to b011f37 Compare August 24, 2026 14:39
@lesandie
lesandie merged commit dea081b into master Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant