Skip to main content

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}