Skip to main content

polydat_core/library/support/
audit.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cycle-time data-source audit log.
5//!
6//! Emits one structured line per significant data-source event so
7//! the operator can spot mismatches between what
8//! `crate::library::vectors::do_dataset_prebuffer` *covered* and
9//! what cycle-time accessors *opened*. The typical failure mode
10//! this catches: prebuffer reports success but readers still hit
11//! HTTP per cycle, because the facets the workload reads aren't
12//! in the active profile's manifest.
13//!
14//! Output is routed through [`set_log_fn`] when the caller (the
15//! activity runner, the test harness, …) installs a sink. With no
16//! sink installed, lines go to stderr — preserves visibility from
17//! contexts that don't carry an `observer::log` plumbing path
18//! (unit tests, the `dryrun=` paths).
19
20use std::sync::OnceLock;
21
22/// Severity for audit-channel events. A conventional severity
23/// ladder so a host's installed sink can map it 1:1 to its own
24/// logger levels without reformatting.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum LogLevel {
27    /// The finest detail.
28    Trace,
29    /// Developer detail.
30    Debug,
31    /// Ordinary events.
32    Info,
33    /// Something is off but the run continues.
34    Warn,
35    /// A failure.
36    Error,
37}
38
39type LogFn = Box<dyn Fn(LogLevel, &str) + Send + Sync>;
40
41static LOG_FN: OnceLock<LogFn> = OnceLock::new();
42
43/// Install the audit sink. A host installs this once so audit lines
44/// flow through its own logger alongside the rest of the run output.
45/// Subsequent calls are no-ops.
46pub fn set_log_fn<F>(f: F)
47where
48    F: Fn(LogLevel, &str) + Send + Sync + 'static,
49{
50    let _ = LOG_FN.set(Box::new(f));
51}
52
53/// Emit a leveled message through the configured sink, falling
54/// back to stderr when no sink is installed (unit tests, dryrun
55/// paths, pre-init).
56pub fn log(level: LogLevel, msg: &str) {
57    if let Some(f) = LOG_FN.get() {
58        f(level, msg);
59    } else {
60        let tag = match level {
61            LogLevel::Trace => "TRC",
62            LogLevel::Debug => "DBG",
63            LogLevel::Info => "INF",
64            LogLevel::Warn => "WRN",
65            LogLevel::Error => "ERR",
66        };
67        eprintln!("{tag} {msg}");
68    }
69}
70
71/// Convenience helpers for callsite ergonomics.
72pub fn debug(msg: &str) {
73    log(LogLevel::Debug, msg);
74}
75/// Log at `Info`.
76pub fn info(msg: &str) {
77    log(LogLevel::Info, msg);
78}
79/// Log at `Warn`.
80pub fn warn(msg: &str) {
81    log(LogLevel::Warn, msg);
82}
83/// Log at `Error`.
84pub fn error(msg: &str) {
85    log(LogLevel::Error, msg);
86}
87
88/// Record that `dataset_prebuffer(...)` was invoked. Emitted at
89/// the top of `do_dataset_prebuffer` *unconditionally* — fires
90/// even if the function bails on a resolve / profile-missing
91/// error, so the absence of this line in `session.log` is
92/// definitive evidence that `init prebuffer = ...` never
93/// evaluated. Pairs with `record_prebuffered` (per-facet) and
94/// `log_prebuffer_summary` (tail).
95pub fn record_prebuffer_entered(source: &str) {
96    debug(&format!("prebuffer: entered dataset_prebuffer({source:?})"));
97}
98
99/// Record that the prebuffer pass covered a facet. Emitted from
100/// inside the `view.prebuffer_all_with_progress` callback, once
101/// per facet the manifest declared.
102pub fn record_prebuffered(source: &str, profile: &str, facet: &str) {
103    debug(&format!("prebuffer: covered {source}:{profile}/{facet}"));
104}
105
106/// Record that a reader was opened for a facet. Emitted from
107/// every `vectors::*` reader-open path *before* the actual
108/// `view.<facet>()` / `open_facet_typed` call so the line lands
109/// even if the open errors. `kind` distinguishes the open shape
110/// (`uniform`, `ivvec32`, `generic-typed`, …) for at-a-glance
111/// debugging.
112pub fn record_opened(source: &str, profile: &str, facet: &str, kind: &str) {
113    debug(&format!(
114        "vectordata: opened {source}:{profile}/{facet} (kind={kind})"
115    ));
116}
117
118/// One-line summary at the end of `dataset_prebuffer`. Pairs
119/// with the per-facet `prebuffer: covered …` lines above and
120/// the `vectordata: opened …` lines below to make the
121/// covered-vs-opened delta easy to read.
122pub fn log_prebuffer_summary(source: &str, profile: &str, facet_count: u64) {
123    debug(&format!(
124        "prebuffer: done {source}:{profile} (covered {facet_count} facet(s))"
125    ));
126}