From d078e0154e8977f20817a0a4cc99a3879b0d10d6 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 14:32:47 +0000 Subject: [PATCH] fix(pilotctl): add size-cap rotation to .pilotctl-audit.log (PILOT-336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root-level pilotctl audit log (.pilotctl-audit.log) had no size cap or rotation mechanism. On long-lived hosts with frequent operator actions, it could grow without bound (\u007e150 B/event, unbounded over multi-year deployments). Add a 100 MiB cap with single-rotation semantics: when the log exceeds 100 MiB, it is renamed to .pilotctl-audit.log.1 and a new active log starts. This matches the existing supervisor.log.1 rotation pattern already consumed by 'pilotctl appstore audit'. The rotation is best-effort only — a rotation failure logs a warning but does not block the audit write, consistent with the rest of writePilotctlAudit's design. Closes PILOT-336 --- cmd/pilotctl/appstore.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cmd/pilotctl/appstore.go b/cmd/pilotctl/appstore.go index 1996cf2b..df28cc79 100644 --- a/cmd/pilotctl/appstore.go +++ b/cmd/pilotctl/appstore.go @@ -1150,6 +1150,12 @@ func cmdAppStoreInstall(args []string) { // as the per-app supervisor.log. const pilotctlAuditFileName = ".pilotctl-audit.log" +// pilotctlAuditMaxSize is the size cap for the root-level audit log. +// When reached, the log is rotated to .pilotctl-audit.log.1 (single +// rotation — only one historical file kept). At ~150 B/event this gives +// roughly 700,000 entries before rotation, or decades of heavy use. +const pilotctlAuditMaxSize = 100 * 1024 * 1024 // 100 MiB + // pilotctlAuditEvent is one row of the root-level audit log. // Operator-side counterpart to the supervisor's auditEvent; lives in // a different file because the use cases differ — supervisor.log @@ -1191,6 +1197,20 @@ func writePilotctlAudit(installRoot string, ev pilotctlAuditEvent) { } body = append(body, '\n') path := filepath.Join(installRoot, pilotctlAuditFileName) + + // Rotate if the log exceeds the size cap. Single-rotation model: + // .pilotctl-audit.log → .pilotctl-audit.log.1, then start fresh. + // Best-effort — rotation failure doesn't block the audit write. + // Matches the supervisor.log.1 pattern so readers that already + // handle supervisor.log rotation can consume this with no changes. + if fi, err := os.Stat(path); err == nil && fi.Size() >= pilotctlAuditMaxSize { + rotatedPath := path + ".1" + if err := os.Rename(path, rotatedPath); err != nil { + fmt.Fprintf(os.Stderr, "warn: pilotctl audit rotate %s → %s: %v\n", + path, rotatedPath, err) + } + } + f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { fmt.Fprintf(os.Stderr, "warn: pilotctl audit open %s: %v\n", path, err)