use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
const MAX_TRACE_ERROR_BYTES: usize = 160;
pub(crate) fn enable() {
let _ = ENABLED.set(true);
}
fn enabled() -> bool {
*ENABLED
.get_or_init(|| std::env::var_os("SAYA_EXTRACTION_TRACE").is_some_and(|v| !v.is_empty()))
}
pub(crate) fn trace_extraction(
outcome: &'static str,
object_count: usize,
proposal_count: Option<usize>,
error: Option<&str>,
elapsed: Option<std::time::Duration>,
) {
if !enabled() {
return;
}
eprintln!(
"{}",
format_trace_line(outcome, object_count, proposal_count, error, elapsed)
);
}
fn format_trace_line(
outcome: &'static str,
object_count: usize,
proposal_count: Option<usize>,
error: Option<&str>,
elapsed: Option<std::time::Duration>,
) -> String {
let proposals = match proposal_count {
Some(n) => format!(" proposals={n}"),
None => String::new(),
};
let err = match error {
Some(e) => format!(" error={}", redacted_trace_error(e)),
None => String::new(),
};
let ms = match elapsed {
Some(d) => format!(" ms={}", d.as_millis()),
None => String::new(),
};
format!("saya extraction: outcome={outcome} objects={object_count}{proposals}{err}{ms}")
}
fn redacted_trace_error(error: &str) -> String {
let safe = saya_types::redact(error)
.chars()
.map(|ch| {
if ch.is_control() && ch != '\t' {
' '
} else {
ch
}
})
.collect::<String>();
if safe.len() <= MAX_TRACE_ERROR_BYTES {
return safe;
}
let marker = "…";
let mut end = MAX_TRACE_ERROR_BYTES.saturating_sub(marker.len());
while end > 0 && !safe.is_char_boundary(end) {
end -= 1;
}
format!("{}{}", &safe[..end], marker)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enable_seeds_the_gate_without_touching_the_environment() {
enable();
assert!(
enabled(),
"--verbose must turn the trace on even with SAYA_EXTRACTION_TRACE unset"
);
enable();
assert!(enabled());
}
#[test]
fn trace_error_is_redacted_and_bounded() {
let sentinel = "provider-json-secret-sentinel";
let detail = format!(
"extraction provider failed password={sentinel} {}\nterminal-noise",
"x".repeat(MAX_TRACE_ERROR_BYTES * 2)
);
let line = format_trace_line("failed", 1, Some(0), Some(&detail), None);
assert!(line.contains("password=[redacted]"));
assert!(!line.contains(sentinel));
assert!(!line.contains('\n'));
assert!(
line.len() < 320,
"trace must remain bounded even for an oversized provider error"
);
}
}