package audit import ( "context" "fmt" "log/slog" "time" ) // Reconcile replays locally-buffered audit records to the central sink // when it is reachable again. A record is removed from the buffer ONLY // after its central write is confirmed, so a crash mid-reconcile re-plays // rather than loses. Returns the number of records reconciled. // // A no-op (0, nil) when the central sink is still unreachable or the // buffer is empty. func Reconcile(ctx context.Context, central Central, buffer Buffer, notifier Notifier) (int, error) { if err := central.Ready(ctx); err != nil { return 0, nil // still down; try again next tick } pending, err := buffer.Pending() if err != nil { return 0, fmt.Errorf("read buffer: %w", err) } reconciled := 0 for _, rec := range pending { if err := central.Push(ctx, rec.Entry); err != nil { // Central went away mid-drain; stop and keep the rest buffered. break } if err := buffer.Confirm(rec.ID); err != nil { return reconciled, fmt.Errorf("confirm buffered record %s: %w", rec.ID, err) } reconciled++ } if reconciled > 0 && notifier != nil { _ = notifier.Notify(ctx, fmt.Sprintf("reconciled %d buffered capture audit record(s) to loki", reconciled)) } return reconciled, nil } // StartReconcile runs Reconcile on a ticker until ctx is cancelled. It is // the recovery half of the degrade-and-buffer path; pair it with a // DegradingSink sharing the same buffer + central. func StartReconcile(ctx context.Context, central Central, buffer Buffer, notifier Notifier, interval time.Duration) { if interval <= 0 { interval = time.Minute } go func() { t := time.NewTicker(interval) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: if n, err := Reconcile(ctx, central, buffer, notifier); err != nil { slog.Warn("audit reconcile failed", "err", err) } else if n > 0 { slog.Info("audit reconcile", "reconciled", n) } } } }() }