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 redacted output.
60///
61/// This marker indicates that the type will produce redacted output when used
62/// with tracing (via `TracingRedactedDebugExt`, `TracingRedactedExt`, or
63/// `TracingValuableExt`).
64///
65/// This trait is implemented only for sink adapters and wrappers that redact
66/// before logging. It is not a blanket impl for raw types.
67pub trait TracingRedacted {}
68
69/// Extension trait for logging structural redacted values as `Debug` fields.
70///
71/// This is the plain `tracing` path for types that derive `Sensitive` or
72/// otherwise implement [`Redactable`]. The helper clones and redacts the value
73/// before it reaches the subscriber, then records the redacted clone through
74/// `tracing::field::debug`.
75pub trait TracingRedactedDebugExt: Redactable + Clone + fmt::Debug {
76    /// Redacts the value and wraps the redacted clone for `tracing` debug
77    /// recording.
78    fn tracing_redacted_debug(&self) -> DebugValue<Self>;
79}
80
81impl<T> TracingRedactedDebugExt for T
82where
83    T: Redactable + Clone + fmt::Debug,
84{
85    fn tracing_redacted_debug(&self) -> DebugValue<Self> {
86        debug(self.clone().redact())
87    }
88}
89
90/// Extension trait for logging redacted values as display strings.
91///
92/// This works with any tracing subscriber but the output is a flat string,
93/// not structured data. For structural `Debug` output, use
94/// [`TracingRedactedDebugExt`]. For structured `valuable` output, see
95/// [`TracingValuableExt`].
96pub trait TracingRedactedExt {
97    /// Wraps the value for `tracing` logging as a display value.
98    ///
99    /// The value is redacted and converted to a string representation.
100    fn tracing_redacted(&self) -> DisplayValue<String>;
101}
102
103impl<T> TracingRedactedExt for T
104where
105    T: ToRedactedOutput,
106{
107    fn tracing_redacted(&self) -> DisplayValue<String> {
108        let output = self.to_redacted_output();
109        let text = match output {
110            RedactedOutput::Text(text) => text,
111            #[cfg(feature = "json")]
112            RedactedOutput::Json(json) => json.to_string(),
113        };
114        display(text)
115    }
116}
117
118impl TracingRedacted for RedactedOutput {}
119
120#[cfg(feature = "json")]
121impl TracingRedacted for RedactedJson {}
122
123impl<T, P> TracingRedacted for SensitiveValue<T, P>
124where
125    T: SensitiveWithPolicy<P>,
126    P: RedactionPolicy,
127{
128}
129
130impl<T> TracingRedacted for NotSensitiveDisplay<T> where T: fmt::Display {}
131
132impl<T> TracingRedacted for NotSensitiveDebug<T> where T: fmt::Debug {}
133
134impl<T> TracingRedacted for NotSensitive<T> where T: TracingRedacted {}
135
136impl<T> TracingRedacted for RedactedOutputRef<'_, T> where T: Redactable + Clone + fmt::Debug {}
137
138#[cfg(feature = "json")]
139impl<T> TracingRedacted for NotSensitiveJson<'_, T> where T: Serialize + ?Sized {}
140
141#[cfg(feature = "json")]
142impl<T> TracingRedacted for RedactedJsonRef<'_, T> where T: Redactable + Clone + Serialize {}
143
144/// A redacted value that implements `valuable::Valuable` for structured tracing output.
145///
146/// This wrapper is constructed by [`TracingValuableExt::tracing_redacted_valuable`]
147/// so callers cannot build structured tracing payloads without first applying
148/// redaction. Pass a reference to the wrapper through `tracing::field::valuable`
149/// when compiling with `RUSTFLAGS="--cfg tracing_unstable"`.
150#[cfg(feature = "tracing-valuable")]
151#[derive(Clone, Debug)]
152pub struct RedactedValuable<T> {
153    redacted: T,
154}
155
156#[cfg(feature = "tracing-valuable")]
157impl<T> RedactedValuable<T> {
158    /// Creates a new `RedactedValuable` from an already-redacted value.
159    pub(crate) fn new(redacted: T) -> Self {
160        Self { redacted }
161    }
162
163    /// Returns a reference to the redacted inner value.
164    pub fn inner(&self) -> &T {
165        &self.redacted
166    }
167}
168
169#[cfg(feature = "tracing-valuable")]
170impl<T: valuable::Valuable> valuable::Valuable for RedactedValuable<T> {
171    fn as_value(&self) -> valuable::Value<'_> {
172        self.redacted.as_value()
173    }
174
175    fn visit(&self, visit: &mut dyn valuable::Visit) {
176        self.redacted.visit(visit);
177    }
178}
179
180#[cfg(feature = "tracing-valuable")]
181impl<T> TracingRedacted for RedactedValuable<T> {}
182
183/// Extension trait for logging redacted values as structured `valuable` data.
184///
185/// This requires the `tracing-valuable` feature, `RUSTFLAGS="--cfg
186/// tracing_unstable"`, and a tracing subscriber that supports the `valuable`
187/// crate. The returned wrapper is not itself a `tracing::Value`; bind it first,
188/// then pass a reference through `tracing::field::valuable`.
189///
190/// # Example
191///
192/// `tracing::field::valuable` is hidden by upstream `tracing` unless the crate
193/// is compiled with `RUSTFLAGS="--cfg tracing_unstable"`, so this example cannot
194/// be compiled by ordinary doctest runs.
195///
196/// ```ignore
197/// use redactable::{Secret, Sensitive};
198/// use redactable::tracing::TracingValuableExt;
199///
200/// #[derive(Clone, Sensitive, valuable::Valuable)]
201/// struct User {
202///     username: String,
203///     #[sensitive(Secret)]
204///     password: String,
205/// }
206///
207/// let user = User { username: "alice".into(), password: "secret".into() };
208///
209/// let redacted = user.tracing_redacted_valuable();
210/// tracing::info!(user = tracing::field::valuable(&redacted));
211/// ```
212#[cfg(feature = "tracing-valuable")]
213pub trait TracingValuableExt {
214    /// The redacted type that will be wrapped in `RedactedValuable`.
215    type Redacted: valuable::Valuable;
216
217    /// Redacts the value and wraps it for structured tracing output.
218    ///
219    /// The returned `RedactedValuable` implements `valuable::Valuable`, allowing
220    /// tracing subscribers to inspect the redacted structure when passed through
221    /// `tracing::field::valuable(&binding)` under `tracing_unstable`.
222    fn tracing_redacted_valuable(&self) -> RedactedValuable<Self::Redacted>;
223}
224
225#[cfg(feature = "tracing-valuable")]
226impl<T> TracingValuableExt for T
227where
228    T: Redactable + Clone + valuable::Valuable,
229{
230    type Redacted = T;
231
232    fn tracing_redacted_valuable(&self) -> RedactedValuable<Self::Redacted> {
233        RedactedValuable::new(self.clone().redact())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    // Mock type for testing TracingRedactedExt
242    struct MockRedactable {
243        value: String,
244    }
245
246    impl ToRedactedOutput for MockRedactable {
247        fn to_redacted_output(&self) -> RedactedOutput {
248            RedactedOutput::Text(format!("[REDACTED:{}]", self.value.len()))
249        }
250    }
251
252    #[test]
253    fn tracing_redacted_converts_to_display_string() {
254        let mock = MockRedactable {
255            value: "secret".into(),
256        };
257        let display_value = mock.tracing_redacted();
258        // DisplayValue wraps the string - we can't easily inspect it,
259        // but we can verify it was created without panicking
260        let _ = format!("{display_value:?}");
261    }
262
263    #[test]
264    fn tracing_redacted_handles_empty_value() {
265        let mock = MockRedactable {
266            value: String::new(),
267        };
268        let display_value = mock.tracing_redacted();
269        let _ = format!("{display_value:?}");
270    }
271
272    #[derive(Clone, Debug)]
273    struct MockStructuralRedactable {
274        password: String,
275    }
276
277    impl crate::redaction::RedactableWithMapper for MockStructuralRedactable {
278        fn redact_with<M: crate::redaction::RedactableMapper>(self, _mapper: &M) -> Self {
279            Self {
280                password: "[REDACTED]".to_string(),
281            }
282        }
283    }
284
285    impl Redactable for MockStructuralRedactable {}
286
287    #[test]
288    fn tracing_redacted_debug_wraps_redacted_clone() {
289        let mock = MockStructuralRedactable {
290            password: "secret".into(),
291        };
292
293        assert_eq!(mock.password, "secret");
294        let debug_value = mock.tracing_redacted_debug();
295        let output = format!("{debug_value:?}");
296
297        assert!(output.contains("[REDACTED]"));
298        assert!(!output.contains("secret"));
299    }
300
301    #[cfg(feature = "tracing-valuable")]
302    mod valuable_tests {
303        use super::*;
304        use crate::redaction::{RedactableMapper, RedactableWithMapper};
305
306        // Mock type that implements both Redactable and Valuable
307        #[derive(Clone, Debug, valuable::Valuable)]
308        struct MockValuableRedactable {
309            username: String,
310            password: String,
311        }
312
313        impl RedactableWithMapper for MockValuableRedactable {
314            fn redact_with<M: RedactableMapper>(self, _mapper: &M) -> Self {
315                // Simple mock: always redact password
316                Self {
317                    username: self.username,
318                    password: "[REDACTED]".to_string(),
319                }
320            }
321        }
322
323        // Manual machinery impls must also declare certification explicitly.
324        impl Redactable for MockValuableRedactable {}
325
326        #[test]
327        fn tracing_redacted_valuable_creates_wrapper() {
328            let mock = MockValuableRedactable {
329                username: "alice".into(),
330                password: "secret".into(),
331            };
332            let valuable = mock.tracing_redacted_valuable();
333
334            // Verify the inner value was redacted
335            assert_eq!(valuable.inner().username, "alice");
336            assert_eq!(valuable.inner().password, "[REDACTED]");
337        }
338
339        #[test]
340        fn redacted_valuable_implements_valuable() {
341            let mock = MockValuableRedactable {
342                username: "alice".into(),
343                password: "secret".into(),
344            };
345            let valuable_wrapper = mock.tracing_redacted_valuable();
346
347            // Verify it implements Valuable by calling as_value
348            let _ = valuable::Valuable::as_value(&valuable_wrapper);
349        }
350    }
351}