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}