Skip to main content

redactable/
tracing.rs

1//! Adapters for emitting redacted values through `tracing`.
2//!
3//! This module provides three explicit tracing paths:
4//!
5//! - **[`TracingRedactedDebugExt`]**: redacts a structural [`Redactable`] value
6//!   before handing its `Debug` form to tracing.
7//!
8//! - **[`TracingRedactedExt`]**: logs `ToRedactedOutput` values as display
9//!   strings. Works with any tracing subscriber but loses structure.
10//!
11//! - **`TracingValuableExt`** (requires the `tracing-valuable` feature and
12//!   `RUSTFLAGS="--cfg tracing_unstable"`): logs redacted values as structured
13//!   data via the `valuable` crate.
14//!
15//! # Example
16//!
17//! ```no_run
18//! # #![allow(hidden_glob_reexports)]
19//! # pub use redactable::*;
20//! use redactable::{Secret, Sensitive, SensitiveValue};
21//! use redactable::tracing::{TracingRedactedDebugExt, TracingRedactedExt};
22//!
23//! #[derive(Clone, Sensitive, serde::Serialize)]
24//! struct User {
25//!     name: String,
26//!     #[sensitive(Secret)]
27//!     token: String,
28//! }
29//!
30//! # fn main() {
31//! let user = User {
32//!     name: "alice".to_owned(),
33//!     token: "api-token".to_owned(),
34//! };
35//! let leaf_token = SensitiveValue::<String, Secret>::from("api-token".to_owned());
36//!
37//! ::tracing::info!(
38//!     user = user.tracing_redacted_debug(),
39//!     leaf_token = leaf_token.tracing_redacted(),
40//! );
41//! # }
42//! ```
43
44use std::fmt;
45
46#[cfg(feature = "json")]
47use serde::Serialize;
48use tracing::field::{DebugValue, DisplayValue, debug, display};
49
50use crate::{
51    policy::RedactionPolicy,
52    redaction::{
53        NotSensitive, NotSensitiveDebug, NotSensitiveDisplay, NotSensitiveJson, Redactable,
54        RedactedJson, RedactedJsonRef, RedactedOutput, RedactedOutputRef, SensitiveValue,
55        SensitiveWithPolicy, ToRedactedOutput,
56    },
57};
58
59/// Marker trait for types whose `tracing` integration always emits logging-safe output.
60///
61/// Implementors are safe because they either redact their value or represent a
62/// value explicitly declared non-sensitive before it reaches tracing (via
63/// `TracingRedactedDebugExt`, `TracingRedactedExt`, or `TracingValuableExt`).
64///
65/// This trait is implemented only for logging-safe adapters and wrappers. It is
66/// not a blanket impl for raw types.
67///
68/// ```compile_fail
69/// use redactable::tracing::TracingRedacted;
70///
71/// fn assert_tracing_redacted<T: TracingRedacted>() {}
72///
73/// assert_tracing_redacted::<String>();
74/// ```
75pub trait TracingRedacted {}
76
77/// Extension trait for logging structural redacted values as `Debug` fields.
78///
79/// This is the plain `tracing` path for types that derive `Sensitive` or
80/// otherwise implement [`Redactable`]. The helper clones and redacts the value
81/// before it reaches the subscriber, then records the redacted clone through
82/// `tracing::field::debug`.
83pub trait TracingRedactedDebugExt: Redactable + Clone + fmt::Debug {
84    /// Redacts the value and wraps the redacted clone for `tracing` debug
85    /// recording.
86    ///
87    /// # Panics
88    ///
89    /// This method inherits every panic from cloning `Self`. In particular, a
90    /// traversed [`std::cell::RefCell`] with a live mutable borrow panics. Use
91    /// [`IntoTracingRedactedDebugExt::into_tracing_redacted_debug`] when the
92    /// original value does not need to be retained.
93    fn tracing_redacted_debug(&self) -> DebugValue<Self>;
94}
95
96/// Consuming extension trait for structural Debug fields in `tracing`.
97///
98/// This adapter redacts the owned value by calling `.redact()` on it instead of cloning it first. It accepts
99/// every [`Redactable`] shape, including types using `#[redactable(recursive)]`.
100///
101/// # Panics
102///
103/// The adapter does not clone the value before redacting (unlike the borrowed adapters), but a type's own `.redact()` may clone internally: traversal through
104/// [`std::sync::Arc`] or [`std::rc::Rc`] must clone the shared referent because
105/// other owners may still hold it, and rebuilding a `HashMap` or `HashSet` clones its `BuildHasher` (a custom hasher whose `Clone` panics or has side effects surfaces here). A live [`std::cell::RefCell`] mutable borrow
106/// behind an `Arc`/`Rc` therefore still panics. Prefer unique ownership
107/// ([`Box`]) for values you log. (`Arc<RefCell<T>>` is `!Send + !Sync` and an
108/// anti-pattern regardless.)
109pub trait IntoTracingRedactedDebugExt: Redactable + fmt::Debug + Sized {
110    /// Consumes and redacts the value before handing it to `tracing`.
111    #[must_use]
112    fn into_tracing_redacted_debug(self) -> DebugValue<Self> {
113        debug(self.redact())
114    }
115}
116
117impl<T> IntoTracingRedactedDebugExt for T where T: Redactable + fmt::Debug {}
118
119impl<T> TracingRedactedDebugExt for T
120where
121    T: Redactable + Clone + fmt::Debug,
122{
123    fn tracing_redacted_debug(&self) -> DebugValue<Self> {
124        debug(self.clone().redact())
125    }
126}
127
128/// Extension trait for logging redacted values as display strings.
129///
130/// This works with any tracing subscriber but the output is a flat string,
131/// not structured data. For structural `Debug` output, use
132/// [`TracingRedactedDebugExt`]. For structured `valuable` output, see
133/// `TracingValuableExt`.
134pub trait TracingRedactedExt {
135    /// Wraps the value for `tracing` logging as a display value.
136    ///
137    /// The value is redacted and converted to a string representation.
138    ///
139    /// # Panics
140    ///
141    /// This method inherits panics from [`ToRedactedOutput`]. Clone-backed
142    /// wrappers such as [`RedactedOutputRef`] and [`RedactedJsonRef`] panic if
143    /// cloning their complete value panics, including a traversed
144    /// [`std::cell::RefCell`] with a live mutable borrow.
145    fn tracing_redacted(&self) -> DisplayValue<String>;
146}
147
148impl<T> TracingRedactedExt for T
149where
150    T: ToRedactedOutput,
151{
152    fn tracing_redacted(&self) -> DisplayValue<String> {
153        let output = self.to_redacted_output();
154        let text = match output {
155            RedactedOutput::Text(text) => text,
156            #[cfg(feature = "json")]
157            RedactedOutput::Json(json) => json.to_string(),
158        };
159        display(text)
160    }
161}
162
163impl TracingRedacted for RedactedOutput {}
164
165#[cfg(feature = "json")]
166impl TracingRedacted for RedactedJson {}
167
168impl<T, P> TracingRedacted for SensitiveValue<T, P>
169where
170    T: SensitiveWithPolicy<P>,
171    P: RedactionPolicy,
172{
173}
174
175impl<T> TracingRedacted for NotSensitiveDisplay<T> where T: fmt::Display {}
176
177impl<T> TracingRedacted for NotSensitiveDebug<T> where T: fmt::Debug {}
178
179impl<T> TracingRedacted for NotSensitive<T> {}
180
181impl<T> TracingRedacted for RedactedOutputRef<'_, T> where T: Redactable + Clone + fmt::Debug {}
182
183#[cfg(feature = "json")]
184impl<T> TracingRedacted for NotSensitiveJson<'_, T> where T: Serialize + ?Sized {}
185
186#[cfg(feature = "json")]
187impl<T> TracingRedacted for RedactedJsonRef<'_, T> where T: Redactable + Clone + Serialize {}
188
189/// A redacted value that implements `valuable::Valuable` for structured tracing output.
190///
191/// This wrapper is constructed by [`TracingValuableExt::tracing_redacted_valuable`]
192/// so callers cannot build structured tracing payloads without first applying
193/// redaction. Pass a reference to the wrapper through `tracing::field::valuable`
194/// when compiling with `RUSTFLAGS="--cfg tracing_unstable"`.
195/// Deliberately NOT `Clone`. Cloning the wrapper would hand out a second handle
196/// to the same redacted value; if that value has shared interior mutability
197/// (e.g. an `Arc`/`Rc` over a `Cell`/`Mutex`), a caller could clone the wrapper,
198/// mutate the shared inner through the clone to insert a *fresh* secret, and
199/// have the original wrapper log it. Without `Clone`, the only way to reach the
200/// inner value is the consuming [`Self::into_inner`], which leaves no original
201/// wrapper behind to log.
202#[cfg(feature = "tracing-valuable")]
203#[derive(Debug)]
204pub struct TracingRedactedValue<T> {
205    redacted: T,
206}
207
208#[cfg(feature = "tracing-valuable")]
209impl<T> TracingRedactedValue<T> {
210    /// Creates a new `TracingRedactedValue` from an already-redacted value.
211    pub(crate) fn new(redacted: T) -> Self {
212        Self { redacted }
213    }
214
215    /// Consumes the wrapper and returns the redacted inner value.
216    ///
217    /// This is deliberately consuming rather than a borrowing `inner(&self)`.
218    /// The original secret is already gone by the time a `TracingRedactedValue`
219    /// exists, but a shared reference to an interior-mutable inner value would
220    /// let a caller write a *fresh* secret into the wrapper after redaction and
221    /// before it is logged. Taking `self` closes that window.
222    #[must_use]
223    pub fn into_inner(self) -> T {
224        self.redacted
225    }
226}
227
228#[cfg(feature = "tracing-valuable")]
229impl<T: valuable::Valuable> valuable::Valuable for TracingRedactedValue<T> {
230    fn as_value(&self) -> valuable::Value<'_> {
231        self.redacted.as_value()
232    }
233
234    fn visit(&self, visit: &mut dyn valuable::Visit) {
235        self.redacted.visit(visit);
236    }
237}
238
239#[cfg(feature = "tracing-valuable")]
240impl<T> TracingRedacted for TracingRedactedValue<T> {}
241
242/// Extension trait for logging redacted values as structured `valuable` data.
243///
244/// This requires the `tracing-valuable` feature, `RUSTFLAGS="--cfg
245/// tracing_unstable"`, and a tracing subscriber that supports the `valuable`
246/// crate. The returned wrapper is not itself a `tracing::Value`; bind it first,
247/// then pass a reference through `tracing::field::valuable`.
248///
249/// # Example
250///
251/// `tracing::field::valuable` is hidden by upstream `tracing` unless the crate
252/// is compiled with `RUSTFLAGS="--cfg tracing_unstable"`, so this example cannot
253/// be compiled by ordinary doctest runs.
254///
255/// ```ignore
256/// use redactable::{Secret, Sensitive};
257/// use redactable::tracing::TracingValuableExt;
258///
259/// #[derive(Clone, Sensitive, valuable::Valuable)]
260/// struct User {
261///     username: String,
262///     #[sensitive(Secret)]
263///     password: String,
264/// }
265///
266/// let user = User { username: "alice".into(), password: "secret".into() };
267///
268/// let redacted = user.tracing_redacted_valuable();
269/// tracing::info!(user = tracing::field::valuable(&redacted));
270/// ```
271#[cfg(feature = "tracing-valuable")]
272pub trait TracingValuableExt {
273    /// The redacted type that will be wrapped in `TracingRedactedValue`.
274    type Redacted: valuable::Valuable;
275
276    /// Redacts the value and wraps it for structured tracing output.
277    ///
278    /// The returned `TracingRedactedValue` implements `valuable::Valuable`, allowing
279    /// tracing subscribers to inspect the redacted structure when passed through
280    /// `tracing::field::valuable(&binding)` under `tracing_unstable`.
281    ///
282    /// # Panics
283    ///
284    /// This method inherits every panic from cloning `Self`. In particular, a
285    /// traversed [`std::cell::RefCell`] with a live mutable borrow panics. Use
286    /// [`IntoTracingRedactedValuableExt::into_tracing_redacted_valuable`] when
287    /// the original value does not need to be retained.
288    fn tracing_redacted_valuable(&self) -> TracingRedactedValue<Self::Redacted>;
289}
290
291/// Consuming extension trait for structured `valuable` tracing output.
292///
293/// This adapter redacts the owned value by calling `.redact()` on it instead of cloning it first. It accepts
294/// every [`Redactable`] shape, including types using `#[redactable(recursive)]`.
295///
296/// # Panics
297///
298/// The adapter does not clone the value before redacting (unlike the borrowed adapters), but a type's own `.redact()` may clone internally: traversal through
299/// [`std::sync::Arc`] or [`std::rc::Rc`] must clone the shared referent because
300/// other owners may still hold it, and rebuilding a `HashMap` or `HashSet` clones its `BuildHasher` (a custom hasher whose `Clone` panics or has side effects surfaces here). A live [`std::cell::RefCell`] mutable borrow
301/// behind an `Arc`/`Rc` therefore still panics. Prefer unique ownership
302/// ([`Box`]) for values you log. (`Arc<RefCell<T>>` is `!Send + !Sync` and an
303/// anti-pattern regardless.)
304#[cfg(feature = "tracing-valuable")]
305pub trait IntoTracingRedactedValuableExt: Redactable + valuable::Valuable + Sized {
306    /// Consumes and redacts the value before wrapping it for `valuable` output.
307    #[must_use]
308    fn into_tracing_redacted_valuable(self) -> TracingRedactedValue<Self> {
309        TracingRedactedValue::new(self.redact())
310    }
311}
312
313#[cfg(feature = "tracing-valuable")]
314impl<T> IntoTracingRedactedValuableExt for T where T: Redactable + valuable::Valuable {}
315
316#[cfg(feature = "tracing-valuable")]
317impl<T> TracingValuableExt for T
318where
319    T: Redactable + Clone + valuable::Valuable,
320{
321    type Redacted = T;
322
323    fn tracing_redacted_valuable(&self) -> TracingRedactedValue<Self::Redacted> {
324        TracingRedactedValue::new(self.clone().redact())
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    // Mock type for testing TracingRedactedExt
333    struct MockRedactable {
334        value: String,
335    }
336
337    impl ToRedactedOutput for MockRedactable {
338        fn to_redacted_output(&self) -> RedactedOutput {
339            RedactedOutput::Text(format!("[REDACTED:{}]", self.value.len()))
340        }
341    }
342
343    #[test]
344    fn tracing_redacted_converts_to_display_string() {
345        let mock = MockRedactable {
346            value: "secret".into(),
347        };
348        let display_value = mock.tracing_redacted();
349        // DisplayValue wraps the string - we can't easily inspect it,
350        // but we can verify it was created without panicking
351        let _ = format!("{display_value:?}");
352    }
353
354    #[test]
355    fn tracing_redacted_handles_empty_value() {
356        let mock = MockRedactable {
357            value: String::new(),
358        };
359        let display_value = mock.tracing_redacted();
360        let _ = format!("{display_value:?}");
361    }
362
363    #[derive(Clone, Debug)]
364    struct MockStructuralRedactable {
365        password: String,
366    }
367
368    impl crate::redaction::RedactableWithMapper for MockStructuralRedactable {
369        fn redact_with<M: crate::redaction::RedactableMapper>(self, _mapper: &M) -> Self {
370            Self {
371                password: "[REDACTED]".to_string(),
372            }
373        }
374    }
375
376    impl Redactable for MockStructuralRedactable {}
377
378    #[test]
379    fn tracing_redacted_debug_wraps_redacted_clone() {
380        let mock = MockStructuralRedactable {
381            password: "secret".into(),
382        };
383
384        assert_eq!(mock.password, "secret");
385        let debug_value = mock.tracing_redacted_debug();
386        let output = format!("{debug_value:?}");
387
388        assert!(output.contains("[REDACTED]"));
389        assert!(!output.contains("secret"));
390    }
391
392    #[cfg(feature = "tracing-valuable")]
393    mod valuable_tests {
394        use super::*;
395        use crate::redaction::{RedactableMapper, RedactableWithMapper};
396
397        // Mock type that implements both Redactable and Valuable
398        #[derive(Clone, Debug, valuable::Valuable)]
399        struct MockValuableRedactable {
400            username: String,
401            password: String,
402        }
403
404        impl RedactableWithMapper for MockValuableRedactable {
405            fn redact_with<M: RedactableMapper>(self, _mapper: &M) -> Self {
406                // Simple mock: always redact password
407                Self {
408                    username: self.username,
409                    password: "[REDACTED]".to_string(),
410                }
411            }
412        }
413
414        // Manual machinery impls must also declare certification explicitly.
415        impl Redactable for MockValuableRedactable {}
416
417        #[test]
418        fn tracing_redacted_valuable_creates_wrapper() {
419            let mock = MockValuableRedactable {
420                username: "alice".into(),
421                password: "secret".into(),
422            };
423            let valuable = mock.tracing_redacted_valuable();
424
425            // Verify the inner value was redacted
426            let inner = valuable.into_inner();
427            assert_eq!(inner.username, "alice");
428            assert_eq!(inner.password, "[REDACTED]");
429        }
430
431        #[test]
432        fn redacted_valuable_implements_valuable() {
433            let mock = MockValuableRedactable {
434                username: "alice".into(),
435                password: "secret".into(),
436            };
437            let valuable_wrapper = mock.tracing_redacted_valuable();
438
439            // Verify it implements Valuable by calling as_value
440            let _ = valuable::Valuable::as_value(&valuable_wrapper);
441        }
442    }
443}