Files
skills/docs/api-stack/api/api-audit-file/src/rotation/rotation.md
2026-05-29 08:20:56 +01:00

3.2 KiB

rotation

(no submodules)

Purpose

Resolves the rotation policy carried by every createFileAuditSink instance and implements the rotation primitive itself — the rename(currentFile → currentFile.<utc-iso>.log) step plus the post-rotation state reset. The module owns both dimensions documented in the spec: size-based (maxSizeBytes, default 100 MiB) and time-based (maxAgeMs, default 24h). Either condition reaching its limit triggers a rotation on the next write.

Rotation lives in a separate module so the sink stays focused on queueing + writing. The retention module reads the policy through the same RotationState so the operator-facing knobs are documented in exactly one place.

Features

  • resolveRotationPolicy(input?) — Zod-equivalent validator returning a frozen RotationPolicy. Defaults filled from the design doc.
  • shouldRotate(state, nowMs) — pure predicate; size OR age satisfies.
  • rotate(fs, basePath, state, clock) — performs the rename, updates state, returns the rotated path for retention to inspect.
  • Raises AUDIT_FILE.ROTATION_FAILED on rename failure with full cause for forensic logging.

Usage: Basic

import {
    resolveRotationPolicy,
    rotate,
    shouldRotate,
} from "@suaveplan/api-audit-file/rotation";

const policy = resolveRotationPolicy({ maxSizeBytes: 1024 * 1024 });
const state = { openedAtMs: Date.now(), bytesWritten: 0, policy };
if (shouldRotate(state, Date.now())) {
    await rotate(fs, "/var/log/audit.log", state, () => Date.now());
}

Usage: Advanced

Disable a dimension by setting it to Number.POSITIVE_INFINITY — useful when an operator wants pure time rotation regardless of size:

const policy = resolveRotationPolicy({
    maxSizeBytes: Number.POSITIVE_INFINITY,
    maxAgeMs: 60 * 60 * 1000, // hourly
});

In tests, drive rotation deterministically with a FakeClock from @suaveplan/testing:

import { FakeClock } from "@suaveplan/testing";

const clock = new FakeClock(0);
const state = { openedAtMs: 0, bytesWritten: 0, policy };
clock.advance(61_000);
await rotate(fs, basePath, state, clock.read);

API Reference

  • interface RotationPolicy
    • maxSizeBytes: number — bytes before size rotation; default 100 MiB.
    • maxAgeMs: number — ms before time rotation; default 24h.
    • maxFiles: number — kept rotated files; default 30.
  • interface RotationState
    • openedAtMs: number — wall-clock open time of current segment.
    • bytesWritten: number — running byte count.
    • policy: RotationPolicy — frozen at construction.
  • resolveRotationPolicy(input?): RotationPolicy
  • shouldRotate(state, nowMs): boolean
  • rotate(fs, basePath, state, clock): Promise<string> — returns the rotated file path.

Implementation Notes

The rotated suffix replaces : with - so the resulting filename is Windows-portable and grep-friendly across shells. State mutation happens AFTER the rename succeeds — a failed rename leaves the original state intact, so the next call retries against the same segment rather than corrupting accounting. bytesWritten resets to 0 instead of re-stat'ing the new empty file; the next successful append updates it in lock-step with the on-disk length.