pub enum AuditSink {
Disabled,
File {
path: PathBuf,
max_bytes: Option<u64>,
state: Mutex<FileState>,
},
}Expand description
Where audit events go.
Disabled unless a path is configured: writing to a file nobody asked for would be a surprising side effect, and the operator is the one who knows where such a file belongs.
Variants§
Implementations§
Source§impl AuditSink
impl AuditSink
Sourcepub fn file(path: impl AsRef<Path>) -> Result<Self>
pub fn file(path: impl AsRef<Path>) -> Result<Self>
Open path for appending, creating it if needed.
Unbounded: rotation is file_with_limit, and the
binary passes DEFAULT_MAX_BYTES through that. This constructor exists
for consumers that manage the file’s size themselves.
Sourcepub fn file_with_limit(
path: impl AsRef<Path>,
max_bytes: Option<u64>,
) -> Result<Self>
pub fn file_with_limit( path: impl AsRef<Path>, max_bytes: Option<u64>, ) -> Result<Self>
Open path, rotating to <path>.1 once it passes max_bytes.
One generation is kept. A trail that grows without bound eventually fills the disk it is meant to protect, and keeping several generations would be a retention policy — which belongs to whoever runs the machine, not to this process.
Sourcepub fn is_enabled(&self) -> bool
pub fn is_enabled(&self) -> bool
Whether anything is being recorded.
Sourcepub async fn record_async(self: &Arc<Self>, event: AuditEvent)
pub async fn record_async(self: &Arc<Self>, event: AuditEvent)
Record one event from an async context, off the runtime’s workers.
record opens, writes and flushes a file, which is
blocking work — the filesystem handlers already thread it into the
spawn_blocking bodies they are running in for that reason, so a slow
disk cannot starve the worker pool that also runs /health and the
accept loop. A handler that has no blocking body of its own has nowhere
to put it and used to call record straight from the runtime thread.
This is that missing half: same write, same ordering.
What this buys and what it costs were both measured rather than reasoned
(tests/blocking_pool.rs). With every blocking thread held, /health
answered in 2.5 µs — the worker threads really are untouched. The same
run had this method take 2.96 s against 1.57 ms once a thread was free:
moving blocking work off the workers does not make it free, it moves it
onto a pool that is shared with every command in flight. So a burst of
concurrent commands does delay an audited response, and that is a
deliberate trade against blocking the accept loop, not an oversight.
Awaited rather than detached, deliberately. Spawning and walking away
would return the response first and leave the entry to land whenever —
or not at all, if the process stops in between. An audit trail that
drops its last entries under load is untrustworthy exactly where it is
load-bearing, which is the same reason record flushes per event.
The hop is skipped entirely when nothing is being recorded: with no
trail configured record returns immediately, and paying for a task
dispatch to do nothing would be a cost on every request of the default
configuration.
Sourcepub fn record(&self, event: AuditEvent)
pub fn record(&self, event: AuditEvent)
Record one event.
Flushed per event rather than buffered until convenient: a trail that loses its last entries when the process dies is least trustworthy exactly when it matters most.
Blocking. From an async context use
record_async, or call this inside a
spawn_blocking body that is already running.