Redactable
redactable is a redaction library for Rust. It lets you mark sensitive data in your structs and enums and produce a safe, redacted version of the same type. Logging and telemetry are the most common use cases, but redaction is not tied to any logging framework.
Table of Contents
- Core traits
- Design philosophy
- How it works
- Walkthrough
- Sensitive vs SensitiveDisplay
- SensitiveDisplay in depth
- Decision guide
- Logging output (explicit boundary)
- Integrations
- Logging with maximum security
- Reference
- License
Core traits
RedactableContainer: composite types (structs, enums) that are traversed field-by-fieldRedactableLeaf: terminal values that can be converted to/from a string for redactionRedactionPolicy: types that define how a leaf is transformed (full redaction, keep last N chars, etc.)
Design philosophy
- Traversal is automatic: nested containers are handled automatically. For
Sensitive, they're walked viaRedactableContainer. ForSensitiveDisplay, they're formatted viaRedactableDisplay. - Redaction is opt-in: leaf values (scalars, strings) pass through unchanged unless explicitly marked with
#[sensitive(Policy)]. Redaction only happens where you ask for it. - Consistent annotation workflow: both
SensitiveandSensitiveDisplayfollow the same pattern - unannotated scalars pass through, unannotated containers are handled via their trait, and#[sensitive(Policy)]applies redaction. - Types are preserved:
Sensitive's.redact()returns the same type, not a string or wrapper.
How it works
The Sensitive derive macro generates traversal code. For each field, it calls RedactableContainer::redact_with. This uniform interface is what makes everything compose.
| Field kind | What happens |
|---|---|
Containers (structs/enums deriving Sensitive) |
Traversal walks into them recursively, visiting each field |
| Unannotated leaves (String, primitives, etc.) | These implement RedactableContainer as a passthrough - they return themselves unchanged |
Annotated leaves (#[sensitive(Policy)]) |
The macro generates transformation code that applies the policy, bypassing the normal RedactableContainer::redact_with call |
Explicit passthrough (#[not_sensitive]) |
Field is left unchanged without requiring RedactableContainer - use for foreign types |
This is why every field must implement RedactableContainer: containers need it for traversal, and leaves provide passthrough implementations that satisfy the requirement without doing anything.
SensitiveDisplay follows the same principle but uses RedactableDisplay instead: nested types format via their fmt_redacted() method, and scalars pass through unchanged. See SensitiveDisplay in depth for details.
Walkthrough
Trait bounds on containers
As described in How it works, every field must implement RedactableContainer. Here's what that looks like in practice:
If a field's type does not implement RedactableContainer, you get a compilation error:
Blanket implementations
Two kinds of types get RedactableContainer for free.
Standard leaf types
String, primitives (u32, bool, etc.) implement RedactableContainer as a passthrough - they return themselves unchanged. This is why unannotated leaves compile and are left as-is:
let profile = Profile ;
let redacted = profile.redact;
assert_eq!;
assert_eq!;
Standard container types
Option, Vec, Box, Arc, etc. implement RedactableContainer by calling redact_with on their inner value(s). They do not change how the inner value is treated: the inner type (and any #[sensitive(...)] on the leaf value) decides whether it is a leaf, a nested container, or classified. Some examples:
Option<String>still treats theStringas a passthrough leafOption<MyStruct>still walks intoMyStruct#[sensitive(Secret)]on anOption<String>leaf applies the policy to the string inside
let outer = Outer ;
let redacted = outer.redact;
assert_eq!; // unchanged
assert_eq!; // walked and redacted
assert_eq!; // policy applied
The #[sensitive(Policy)] attribute
The #[sensitive(Policy)] attribute marks a leaf as sensitive and applies a redaction policy. When present, the derive macro generates transformation code that applies the policy directly, bypassing the normal redact_with passthrough:
#[sensitive(Secret)]on scalars: replaces the value with a default (0, false,'*')#[sensitive(Secret)]on strings: replaces with"[REDACTED]"#[sensitive(Policy)]on strings: applies the policy's redaction rules
⚠️ Qualified primitive paths don't work with #[sensitive(Secret)]
The derive macro decides how to handle #[sensitive(Secret)] based on a syntactic check of how you wrote the type. Only bare primitive names like u32, bool, char are recognized as scalars. Qualified paths like std::primitive::u32 are not.
This matters because:
- Unannotated leaves: Both
u32andstd::primitive::u32work identically (passthrough viaRedactableContainer) #[sensitive(Secret)]leaves:u32→ recognized as scalar → redacts to0✅std::primitive::u32→ not recognized → tries to usePolicyApplicable→ compile error ❌
Workaround: Always use bare primitive names (u32, bool, etc.) when applying #[sensitive(Secret)].
How RedactableLeaf fits in
When you write #[sensitive(Policy)], the generated code needs to:
- Extract a string from the value (to apply the policy)
- Reconstruct the original type from the redacted string (so you get back your original type, not
String)
RedactableLeaf provides this interface:
use RedactableLeaf;
;
String already implements RedactableLeaf, which is why #[sensitive(Token)] works on String leaves out of the box. Implement it for your own types if you want policies to work on them.
Opting out with NotSensitive
Some types you own need to satisfy RedactableContainer bounds but have no sensitive data. Use #[derive(NotSensitive)] to generate a no-op RedactableContainer impl:
use ;
Note:
NotSensitiveis for types used with theSensitivederive. ForSensitiveDisplay, you have two options: use#[derive(NotSensitiveDisplay)]for a type with no sensitive data that needs logging integration (symmetric withSensitiveDisplay), or use the#[not_sensitive]field attribute when only some fields need to opt out.
NotSensitiveDisplay - Full logging integration
When you need logging integration (slog, tracing) for a type with no sensitive data, use NotSensitiveDisplay. This is the display counterpart to NotSensitive:
use NotSensitiveDisplay;
/// Retry using backoff
NotSensitiveDisplay generates:
RedactableContainer- no-op passthrough (allows use insideSensitivecontainers)RedactableDisplay- delegates toDisplay::fmtDebug- production usesDisplay, test builds use standardDebug(use#[not_sensitive_display(skip_debug)]to opt out)slog::ValueandSlogRedacted- whenslogfeature is enabledTracingRedacted- whentracingfeature is enabled
This is useful when combining with displaydoc or similar crates that derive Display:
use NotSensitiveDisplay;
// Now RetryDecision has Display (from displaydoc), RedactableDisplay, slog::Value, etc.
Wrapper types for foreign types
Two wrapper types handle types you don't own (Rust's orphan rules prevent deriving Sensitive or implementing RedactableLeaf on foreign types):
NotSensitiveValue<T>: Wraps T and passes through unchangedSensitiveValue<T, P>: Wraps T and applies policy P when redacted
Foreign types with no sensitive data
Use NotSensitiveValue<T> to satisfy RedactableContainer bounds:
use ;
// (pretend this is from another crate)
Foreign leaf types that need redaction
For string-like foreign types (IDs, tokens), use RedactableWithPolicy<P> with SensitiveValue<T, P>:
// ❌ ERROR: can't implement RedactableLeaf (foreign trait) for ForeignId (foreign type)
// ✅ OK: RedactableWithPolicy<MyPolicy> is "local enough" because MyPolicy is yours
Then wrap the leaf:
Here's a complete example:
use ;
; // (pretend this comes from another crate)
// 1. Define a local policy (can reuse built-in logic)
;
// 2. Implement RedactableWithPolicy for the foreign type
// 3. Create a type alias for ergonomics
type SensitiveForeignId = ;
// 4. Use the alias
let wrapped = from;
⚠️ Wrappers treat their inner type as a leaf, not a container. Neither walks nested containers - if T derives Sensitive, its internal #[sensitive(...)] annotations would not be applied. This is ok because if a type derives Sensitive it should not be wrapped.
💡 These wrappers can also be used for types you own to provide additional logging safety guarantees. See Logging with maximum security for details.
Sensitive vs SensitiveDisplay
There are two derive macros for redaction, plus their non-sensitive counterparts. Pick the one that matches your constraints:
Sensitive |
SensitiveDisplay |
|
|---|---|---|
| Output | Same type with redacted leaves | Redacted string |
| Ownership | Consumes self (clone if you need the original) |
Borrows self |
| Traverses containers | Yes (walks all fields) | No (only template placeholders) |
| Unannotated scalars | Passthrough | Passthrough |
| Unannotated containers | Walked via RedactableContainer |
Formatted via RedactableDisplay |
| Best for | Structured data | Display strings, non-Clone types |
Sensitive (structured redaction)
Use Sensitive when you can consume the value (or clone it if you need the original). Nested containers are traversed automatically; leaves are only redacted when annotated with #[sensitive(Policy)].
use Sensitive;
let attempt = LoginAttempt ;
let redacted = attempt.redact;
assert_eq!;
assert_eq!;
SensitiveDisplay (string formatting)
Use SensitiveDisplay when you need a redacted string representation without Clone. It formats from a template and uses RedactableDisplay for unannotated placeholders. Common scalar-like types implement RedactableDisplay as passthrough.
use SensitiveDisplay;
let err = Invalid ;
// err.redacted_display() → "login failed for alice [REDACTED]"
SensitiveDisplay also derives a conditional Debug impl: in production builds it formats via
RedactableDisplay::fmt_redacted, while test/testing builds show actual values for debugging.
Use #[sensitive(skip_debug)] to opt out if you need a custom Debug.
See SensitiveDisplay in depth for template syntax and field annotations.
Nested SensitiveDisplay types are redacted automatically without extra annotations:
use ;
let err = RequestFailed ;
// err.redacted_display() → "request failed: db password [REDACTED]"
SensitiveDisplay in depth
SensitiveDisplay derives RedactableDisplay, which provides fmt_redacted() and redacted_display(). Unlike Sensitive, it produces a string rather than a redacted copy of the same type.
The annotation workflow mirrors Sensitive:
- Unannotated scalars → passthrough (unchanged)
- Unannotated nested types → use their
RedactableDisplayimplementation #[sensitive(Policy)]→ apply redaction policy
Template syntax
The display template comes from one of two sources:
1. #[error("...")] attribute (thiserror-style):
2. Doc comment (displaydoc-style):
Both support:
- Named placeholders:
{field_name} - Positional placeholders:
{0},{1} - Debug formatting:
{field:?}
Field annotations
Unannotated placeholders use RedactableDisplay:
| Annotation | Behavior |
|---|---|
| (none) | Uses RedactableDisplay: scalars pass through unchanged; nested SensitiveDisplay types are redacted |
#[not_sensitive] |
Renders with raw Display (or Debug if {:?}) - use for types without RedactableDisplay |
#[sensitive(Secret)] |
Scalars → default value; strings → "[REDACTED]" |
#[sensitive(Policy)] |
Applies the policy's redaction rules |
This matches Sensitive behavior: scalars pass through, nested containers use their redaction trait. Both Sensitive and SensitiveDisplay support all these annotations.
Unannotated fields that do not implement RedactableDisplay produce a compile error:
;
This prevents accidental exposure when adding new fields while still making nested redaction ergonomic.
Decision guide
Which derive macro?
| Situation | Use |
|---|---|
| Structured data you can consume (or clone) | #[derive(Sensitive)] |
| Types you only want to borrow | #[derive(SensitiveDisplay)] |
Type with no sensitive data (for Sensitive bounds) |
#[derive(NotSensitive)] |
Type with no sensitive data (for SensitiveDisplay bounds / logging) |
#[derive(NotSensitiveDisplay)] |
Error types are a common case: use Sensitive if your error type implements Clone, otherwise use SensitiveDisplay. For non-sensitive types that need logging integration, use NotSensitiveDisplay.
How to opt out of redaction?
| Context | Situation | Use |
|---|---|---|
Sensitive |
Type you own, no sensitive data | #[derive(NotSensitive)] |
Sensitive |
Foreign type in a field | #[not_sensitive] attribute or NotSensitiveValue<T> wrapper |
SensitiveDisplay |
Type you own, no sensitive data + need logging | #[derive(NotSensitiveDisplay)] |
SensitiveDisplay |
Field without RedactableDisplay |
#[not_sensitive] attribute (uses raw Display) |
How to handle foreign types that need redaction?
| Situation | Use |
|---|---|
| Foreign leaf type with policy | SensitiveValue<T, Policy> + RedactableWithPolicy |
How to produce logging output?
| Situation | Use |
|---|---|
| Container → redacted text | .redacted_output() |
| Container → redacted JSON | .redacted_json() (requires json feature) |
| Non-sensitive value (explicit Display) | .not_sensitive_display() |
| Non-sensitive value (explicit Debug) | .not_sensitive_debug() |
| Non-sensitive value (explicit JSON) | .not_sensitive_json() (requires json feature) |
| Non-sensitive value (delegate to framework) | .not_sensitive() |
| SensitiveDisplay type | .redacted_display() or .to_redacted_output() |
Which not_sensitive method should I use?
| Method | Trait requirement | Returns | Use when |
|---|---|---|---|
.not_sensitive() |
None | NotSensitive<&Self> |
Type already implements slog::Value or you want the logging framework to decide formatting |
.not_sensitive_display() |
T: Display |
NotSensitiveDisplay<&T> |
You want explicit Display formatting, works with any type |
.not_sensitive_debug() |
T: Debug |
NotSensitiveDebug<&T> |
You want explicit Debug formatting, works with any type |
.not_sensitive_json() |
T: Serialize |
NotSensitiveJson<&T> |
You want structured JSON output (requires json feature) |
Important: .not_sensitive() is a thin wrapper that delegates formatting to the logging framework. It only works with slog when T: slog::Value. If your type doesn't implement slog::Value, use .not_sensitive_display() or .not_sensitive_debug() instead - they work with any type that implements Display or Debug respectively.
Logging output (explicit boundary)
ToRedactedOutput is the single logging-safe bound. It produces a RedactedOutput:
RedactedOutput::Text(String)RedactedOutput::Json(serde_json::Value)(requires thejsonfeature)
⚠️ The Json variant uses serde_json::Value, which integrates well with slog's structured logging. For tracing, the Json variant is converted to a string since tracing's Value trait is sealed.
Several wrappers produce RedactedOutput:
SensitiveValue<T, Policy>(Text)RedactedOutputRef/.redacted_output()(Text)RedactedJsonRef/.redacted_json()(Json,jsonfeature)NotSensitiveDisplay/.not_sensitive_display()(Text)NotSensitiveDebug/.not_sensitive_debug()(Text)NotSensitiveJson/.not_sensitive_json()(Json,jsonfeature)
Use .not_sensitive() when the type already implements slog::Value or you want to
delegate formatting to the logging framework. It borrows &self, returning NotSensitive<&Self>.
The wrapper:
- Implements
Deref<Target = T>for ergonomic access - Implements
DisplaywhenT: DisplayandDebugwhenT: Debug - Implements
slog::ValuewhenT: slog::Value(delegates to inner value's serialization) - Does not implement
ToRedactedOutput(use.not_sensitive_display()or.not_sensitive_debug()if you need that)
⚠️ If your type doesn't implement slog::Value, using .not_sensitive() with slog will give
a compile error. In that case, use .not_sensitive_display() or .not_sensitive_debug() instead,
which work with any Display or Debug type respectively.
Wrapper safety at the boundary
Types that guarantee redaction for a sink implement the sink marker traits
(SlogRedacted, TracingRedacted). In practice:
- Guaranteed redaction (by definition or adapter):
SensitiveValue<T, Policy>,RedactedOutput,RedactedOutputRef,RedactedJsonRef(json), plus sink-specific wrappers likeslog::RedactedJsonandtracing::RedactedValuable(withtracing-valuable) - Derived types:
Sensitive,SensitiveDisplay, andNotSensitiveDisplayimplement the marker traits when the sink feature is enabled - Explicitly non-sensitive:
NotSensitive<T>(whenT: SlogRedacted/TracingRedacted),NotSensitiveDisplay<T>,NotSensitiveDebug<T>,NotSensitiveJson<T>wrapper types (you are asserting safety) - Not a guarantee: raw
String/scalars and passthroughRedactableDisplaytypes
use ;
;
let event = Event ;
log_redacted;
log_redacted;
log_redacted;
log_redacted;
let safe_status = event.status.not_sensitive;
let _ = format!;
let _ = format!;
log_redacted;
log_redacted;
Notes:
redacted_output()usesDebugformatting on the redacted value;redacted_json()provides structured output when JSON is available- This crate does not override
Display, so bypassingToRedactedOutputand logging raw values directly can still leak data - For stronger guarantees, route all logging through helpers that require
T: ToRedactedOutput
Integrations
slog
The slog feature enables automatic redaction - just log your values and they're redacted:
[]
= { = "0.5", = ["slog"] }
Containers - the Sensitive derive generates slog::Value automatically:
let event = PaymentEvent ;
// Just log it - slog::Value impl handles redaction automatically
info!;
// Logged JSON: {"customer_email":"al***@example.com","card_number":"************1234","amount":9999}
Leaf wrappers - SensitiveValue<T, P> also implements slog::Value:
let api_token: = from;
// Also automatic - SensitiveValue has its own slog::Value impl
info!;
// Logged: "*********-key"
Both work because they implement slog::Value - containers via the derive macro, wrappers via a manual implementation. No explicit conversion needed. SensitiveDisplay and NotSensitiveDisplay types also derive slog::Value when the feature is enabled, emitting the (redacted) display string.
tracing
For structured logging with tracing, use the valuable integration:
[]
= { = "0.5", = ["tracing-valuable"] }
use TracingValuableExt;
let event = AuthEvent ;
// Redacts and logs as structured data - subscriber can traverse containers
info!;
// Logged: {api_key: "***************2345", user_email: "al***@example.com", action: "login"}
Unlike slog where slog::Value can be implemented automatically via the derive macro, tracing's Value trait is sealed. The valuable crate provides the structured data path - .tracing_redacted_valuable() redacts first, then wraps for valuable inspection.
For individual values (without valuable):
use TracingRedactedExt;
let api_key: = from;
let user_email: = from;
info!;
// Logged: api_key="***************2345" user_email="al***@example.com" action="login"
⚠️ Note: The valuable integration in tracing is still marked as unstable and requires a compatible subscriber.
Sink-specific safety traits
SlogRedacted and TracingRedacted are marker traits that certify a type's output is redacted
for a specific sink. They indicate that the sink adapter uses the redacted path; they do not
validate policy choices.
They live in redactable::slog and redactable::tracing because the adapters differ
(slog::Value JSON vs tracing display/valuable). A type might be safe for one sink and not
the other.
The traits are implemented only next to the sink adapters (derive-generated impls and specific
wrappers), not as blanket impls for raw types or ToRedactedOutput. See
Wrapper safety at the boundary for the covered wrappers.
Logging with maximum security
For high-security domains (finance, healthcare, compliance-sensitive systems), you need guarantees that sensitive data can't be accidentally logged. This section explains the safety guarantees the library provides and how to leverage them.
The logging footgun
With #[sensitive(P)] attributes, the field is still the bare type at runtime:
let user = User ;
// ❌ Nothing stops you from logging the field directly
info!; // Logs "alice@example.com" unredacted!
This is the core problem: #[sensitive(P)] marks intent but doesn't change the runtime type.
Built-in safety with slog and tracing
The library provides automatic safety when you use the slog or tracing integrations correctly. Types deriving Sensitive or SensitiveDisplay automatically implement slog::Value and the SlogRedacted/TracingRedacted marker traits.
slog - Just log containers directly:
let user = User ;
// ✅ Safe: slog::Value impl auto-redacts before logging
info!;
// Logged: {"email":"al***@example.com"}
tracing - Use the extension traits:
use TracingValuableExt;
// ✅ Safe: redacts before logging as structured data
info!;
The footgun only happens when you bypass these integrations by logging individual fields directly (user.email instead of &user).
Enforcing safety with trait bounds
The library provides marker traits that certify a type's output is redacted for a specific sink:
SlogRedacted- implemented by types safe to log via slogTracingRedacted- implemented by types safe to log via tracing
These traits are implemented for:
- Types deriving
Sensitive,SensitiveDisplay, orNotSensitiveDisplay(when the feature is enabled) SensitiveValue<T, P>wrappersRedactedOutput,RedactedJson,RedactedOutputRef,RedactedJsonRefNotSensitive<T>whenTimplements the marker trait (you assert safety)NotSensitiveDisplay<T>,NotSensitiveDebug<T>,NotSensitiveJson<T>wrapper types (you assert safety)
Use these traits as bounds to enforce safety in your own logging macros:
use SlogRedacted;
// Macro that only accepts types certified as slog-safe
// ✅ Works: Sensitive-derived types implement SlogRedacted
slog_safe!;
// ✅ Works: SensitiveValue implements SlogRedacted
slog_safe!; // SensitiveValue<String, Token>
// ❌ Won't compile: raw String doesn't implement SlogRedacted
slog_safe!;
For tracing:
use TracingRedacted;
Alternative: SensitiveValue<T, P> wrappers
If you want field-level protection even outside the logging integrations, use SensitiveValue<T, P> wrappers instead of #[sensitive(P)] attributes:
let user = User ;
// ✅ Safe: Debug shows the policy-redacted value, not the raw email
info!;
// ✅ Safe: explicit call for redacted form
info!;
// ⚠️ Intentional: .expose() for raw access (code review catches this)
let raw = user.email.expose;
Trade-offs: attributes vs wrappers
#[sensitive(P)] |
SensitiveValue<T, P> |
|
|---|---|---|
| Ergonomics | ✅ Work with actual types | ❌ Need .expose() everywhere |
Display ({}) |
Shows raw value | ✅ Not implemented (won't compile) |
Debug ({:?}) |
✅ Shows [REDACTED]* |
✅ Shows policy-redacted value |
| Serialization | Shows raw value | Shows raw value |
| slog/tracing safety | ✅ Via container | ✅ Direct |
* The Sensitive and SensitiveDisplay derives generate Debug impls that show [REDACTED]
for sensitive data (disabled in test mode via cfg(test) or feature = "testing").
⚠️ Neither approach protects serialization. Both serialize to raw values. This is intentional: serialization is used for API responses, database persistence, message queues, etc. If you need redacted serialization, call .redact() before serializing.
Choosing an approach
- Use the slog/tracing integrations - Log containers via
&user(slog) or.tracing_redacted_valuable()(tracing). Safety is automatic. - Use
SlogRedacted/TracingRedactedbounds - Enforce safety in your own helpers. Only certified types compile. - Use
SensitiveValue<T, P>wrappers - When you need field-level protection outside logging, or can't control the logging layer. - Use
#[sensitive(P)]attributes - Most ergonomic. Safe when logging containers through the integrations.
Reference
Trait map
Domain layer (what is sensitive):
| Trait | Purpose | Implemented By |
|---|---|---|
RedactableContainer |
Walkable containers | Structs/enums deriving Sensitive/NotSensitive/NotSensitiveDisplay, NotSensitiveValue<T>, standard leaves (passthrough), standard containers (Option, Vec, etc.) |
RedactableLeaf |
String-like leaves | String, Cow<str>, custom newtypes |
Policy layer (how to redact):
| Trait | Purpose | Implemented By |
|---|---|---|
RedactionPolicy |
Maps policy marker -> redaction | Built-in policies (Secret, Token, Email, etc.) and your custom policies |
TextRedactionPolicy |
Concrete string transformations | Built-ins (Full/Keep/Mask) |
Application layer (redaction machinery):
| Trait | Purpose | Implemented By |
|---|---|---|
PolicyApplicable |
Applies policy through wrappers | String, Option, Vec, etc. |
Redactable |
User-facing .redact() |
Auto-implemented for RedactableContainer |
RedactableWithPolicy |
Policy-aware leaf redaction | RedactableLeaf types and external types |
ToRedactedOutput |
Logging output boundary | RedactedOutput, RedactedJson, SensitiveValue<T,P>, RedactedOutputRef, RedactedJsonRef, NotSensitiveDisplay<T>, NotSensitiveDebug<T>, NotSensitiveJson<T>, RedactableDisplay types |
RedactableMapper |
Internal traversal | #[doc(hidden)] |
Types:
| Type | Purpose |
|---|---|
RedactedOutput |
Enum for logging output: Text(String) or Json(serde_json::Value) |
RedactedJson |
Owned redacted JSON output (requires json feature) |
SensitiveValue<T, P> |
Wrapper that applies policy P to leaf type T |
NotSensitiveValue<T> |
Wrapper that passes T through unchanged |
NotSensitive<T> |
Wrapper for non-sensitive values that delegates to T's formatting/slog impl (via .not_sensitive()) |
NotSensitiveDisplay<T> |
Wrapper using Display formatting at logging boundaries (via .not_sensitive_display()) |
NotSensitiveDebug<T> |
Wrapper using Debug formatting at logging boundaries (via .not_sensitive_debug()) |
NotSensitiveJson<T> |
Wrapper using JSON serialization at logging boundaries (via .not_sensitive_json(), requires json feature) |
Display/logging layer:
| Trait | Purpose | Implemented By |
|---|---|---|
RedactableDisplay |
Redacted string formatting | SensitiveDisplay derive, NotSensitiveDisplay derive, scalars (passthrough), containers (delegate to contents) |
SlogRedacted |
slog redaction safety marker | Derived types and safe slog wrappers |
TracingRedacted |
tracing redaction safety marker | Derived types and safe tracing wrappers |
SlogRedactedExt |
slog structured JSON logging | Types implementing Redactable + Debug + Serialize |
TracingRedactedExt |
tracing display string logging | Types implementing ToRedactedOutput |
TracingValuableExt |
tracing structured logging via valuable |
Types implementing Redactable + Clone + Valuable |
Supported types
Leaves (implement RedactableLeaf):
String,Cow<'_, str>- Custom newtypes (implement
RedactableLeafyourself) - Note:
&stris not supported forSensitive; use owned strings orCow
Scalars (with #[sensitive(Secret)]):
- Integers →
0, floats →0.0,bool→false,char→'*'
Scalars (implement RedactableDisplay as passthrough):
String,str,bool,char, integers, floats,Cow<str>,PhantomData,()- NonZero integers:
NonZeroI8/I16/I32/I64/I128/Isize,NonZeroU8/U16/U32/U64/U128/Usize - std::time:
Duration,Instant,SystemTime std::cmp::Ordering- Feature-gated:
chronotypes (includingDuration,Month,Weekday),timetypes (includingDuration,UtcOffset,Month,Weekday),Uuid
Containers (implement RedactableContainer and RedactableDisplay):
Option<T>,Vec<T>,VecDeque<T>,Box<T>,Arc<T>,Rc<T>,Result<T, E>, slices[T]HashMap,BTreeMap,HashSet,BTreeSetCell<T>,RefCell<T>- For
Sensitive: walked automatically; policy annotations apply through them - For
SensitiveDisplay: formatted viaRedactableDisplay, delegating to inner types - Map keys are formatted with
Debugand are not redacted
Opaque types (feature-gated):
serde_json::Value(requiresjsonfeature): treated as a leaf that fully redacts toValue::String("[REDACTED]"). This is safe-by-default - unannotatedValuefields are fully redacted because the dynamic structure could contain anything sensitive.
External types: NotSensitiveValue<T> for passthrough, SensitiveValue<T, Policy> with RedactableWithPolicy for redaction.
Precedence and edge cases
#[sensitive(Policy)] on strings works with String and Cow<str> (and their wrappers like Option<String>). Scalars can only use #[sensitive(Secret)]. For custom types, use the SensitiveValue<T, Policy> wrapper instead.
A type can implement both RedactableLeaf and derive Sensitive. This is useful when you want the option to either traverse the type's containers or redact it as a unit depending on context. Which trait is used depends on how the value is declared:
- Bare type (unannotated): uses
RedactableContainer, containers are traversed SensitiveValue<T, Policy>wrapper: usesRedactableLeaf, redacted as a unit
Unannotated containers whose type derives Sensitive are still walked. If a nested type has #[sensitive(Policy)] annotations on its leaves, those are applied even when the outer container is unannotated.
Implementing RedactableLeaf on a struct or enum makes it a terminal value. Its fields will not be traversed or individually redacted. This is useful when you want to redact the entire value as a unit, but nested #[sensitive(Policy)] annotations inside that type are ignored when it's used as a leaf.
Sets can collapse after redaction. HashSet/BTreeSet are redacted element-by-element and then collected back into a set. If redaction makes elements equal (e.g., multiple values redact to "[REDACTED]"), the resulting set may shrink. If cardinality matters, prefer a Vec.
Built-in policies
| Policy | Use for | Example output |
|---|---|---|
Secret |
Scalars or generic redaction | 0 / false / '*' / [REDACTED] |
Token |
API keys | ...f456 (last 4) |
Email |
Email addresses | al***@example.com |
CreditCard |
Card numbers | ...1234 (last 4) |
Pii |
Generic PII (names, addresses) | ...oe (last 2) |
PhoneNumber |
Phone numbers | ...4567 (last 4) |
IpAddress |
IP addresses | ....100 (last 4) |
BlockchainAddress |
Wallet addresses | ...abcdef (last 6) |
Custom policies
use ;
;
License
Licensed under the MIT license (LICENSE.md or opensource.org/licenses/MIT).