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}