santh_tracing/redacting_writer.rs
1//! A [`std::io::Write`] adapter that redacts secrets from log output.
2//!
3//! The Santh safe-defaults contract requires that secrets are never written
4//! to logs. `RedactingWriter` wraps any writer and runs each complete line
5//! through [`santh_error::redact_secrets`] - the single, canonical redaction
6//! routine shared across the ecosystem - before forwarding it downstream.
7//! Partial (newline-less) input is buffered and redacted on [`flush`](std::io::Write::flush)
8//! so a secret split across writes is never emitted in the clear.
9
10use std::io::{self, Write};
11
12use santh_error::redact_secrets;
13
14/// Wraps a writer and redacts known secret shapes from every line written
15/// through it. Redaction is line-oriented: complete lines are redacted and
16/// forwarded immediately; a trailing partial line is held until the next
17/// newline or until [`flush`](Write::flush).
18pub struct RedactingWriter<W: Write> {
19 inner: W,
20 pending: Vec<u8>,
21}
22
23impl<W: Write> RedactingWriter<W> {
24 /// Wrap `inner`, redacting secrets from everything written through it.
25 pub fn new(inner: W) -> Self {
26 Self {
27 inner,
28 pending: Vec::new(),
29 }
30 }
31
32 /// Redact and forward every complete (newline-terminated) line currently
33 /// buffered, leaving any trailing partial line pending.
34 fn forward_complete_lines(&mut self) -> io::Result<()> {
35 while let Some(newline) = self.pending.iter().position(|&b| b == b'\n') {
36 let line: Vec<u8> = self.pending.drain(..=newline).collect();
37 let redacted = redact_secrets(&String::from_utf8_lossy(&line));
38 self.inner.write_all(redacted.as_bytes())?;
39 }
40 Ok(())
41 }
42}
43
44impl<W: Write> Write for RedactingWriter<W> {
45 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
46 self.pending.extend_from_slice(buf);
47 self.forward_complete_lines()?;
48 Ok(buf.len())
49 }
50
51 fn flush(&mut self) -> io::Result<()> {
52 if !self.pending.is_empty() {
53 let redacted = redact_secrets(&String::from_utf8_lossy(&self.pending));
54 self.inner.write_all(redacted.as_bytes())?;
55 self.pending.clear();
56 }
57 self.inner.flush()
58 }
59}