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 for logging and telemetry. Redaction is not tied to any logging framework.
Table of Contents
- Getting started
- Design principles
- How Sensitive works
- How SensitiveDisplay works
- NotSensitive and NotSensitiveDisplay
- Wrapper types
- Integrations
- Logging safety
- Choosing what to use
- Reference
Getting started
There are two derive macros for types with sensitive data. Which one to use depends on whether you need the redacted result as a structured value or as a string.
Use Sensitive when you need the redacted value as a structured type. E.g. .redact() returns a User with redacted fields, not a string. The result can be serialized to JSON, passed to slog, inspected via valuable in tracing, or consumed by anything that works with typed data.
Use SensitiveDisplay when you need the redacted value as a formatted string. .redacted_display() (from the RedactableWithFormatter trait) returns a displayable value with sensitive parts replaced — format it directly or call .to_string(). This is the natural fit for error messages, display output, flat log lines, and any context that expects text.
Quick examples
Structured (Sensitive), logged as JSON:
use ;
use Serialize;
let user = User ;
let redacted = user.clone.redact;
assert_eq!;
assert_eq!;
// slog: automatic redaction, logged as structured JSON
info!;
// → {"name":"alice","email":"al***@example.com"}
String (SensitiveDisplay), logged as text:
use ;
let err = InvalidCredentials ;
assert_eq!;
What each derive generates
| Derive | Output | Debug |
Logging |
|---|---|---|---|
Sensitive |
Same type with redacted leaves (via RedactableWithMapper) |
✅ (redacted) | ✅ |
SensitiveDisplay |
Redacted string (via RedactableWithFormatter) |
✅ (redacted) | ✅ |
-
Both generate a conditional
Debugimpl: redacted output by default, actual values in your crate'scfg(test)builds or when theredactable/testingfeature is enabled. This means all field types must implementDebug. -
Both generate
slog::Value+SlogRedacted(requiresslogfeature) andTracingRedacted(requirestracingfeature).Sensitiveemits structured JSON via slog (requiresSerialize).SensitiveDisplayemits the redacted display string. -
Deriving
Sensitiveand calling consuming.redact(self)do not requireClone. A clone is required only by APIs that redact from a borrowed value while preserving the original, and by container implementations whose ownership requires it.SensitiveDisplayworks by reference. -
SensitiveDisplaydoes not generateRedactableWithMapper. If a type needs both structural traversal and display formatting (e.g., a newtype that lives inside aSensitivecontainer but also needs.redacted_display()), derive both on the same type with#[sensitive(dual)]:/// {0} ] String);#[sensitive(dual)]coordinates both macros:SensitiveskipsDebug(lettingSensitiveDisplayprovide it) andSensitiveDisplayskipsslog/tracing(lettingSensitiveprovide them). Each macro generates only its non-overlapping impls. Usingdualwith only one of the two derives is a compile error naming the missing counterpart. This missing-pair guard does not currently fire for generic types, so generic dual-derive types must keep both derives together by convention and tests.
Design principles
The library follows three principles:
- Redaction should be opt-in. No data is redacted unless you explicitly mark it with
#[sensitive(Policy)]. Unannotated fields pass through unchanged. You choose what to protect and how. - Traversal should be automatic. Nested containers are walked recursively without manual intervention. For
Sensitive, this happens viaRedactableWithMapper. ForSensitiveDisplay, viaRedactableWithFormatter. - Both paths should share the same annotation model. Whether you use
SensitiveorSensitiveDisplay, the workflow is identical: unannotated fields pass through, containers delegate to their trait, and#[sensitive(Policy)]applies redaction.
serde_json::Value is the main built-in traversal exception: with the json feature enabled, an unannotated Value redacts to "[REDACTED]" during .redact() traversal and in adapters that invoke that traversal, because dynamic JSON can carry arbitrary sensitive data. Direct derive-generated Debug remains annotation-driven, so this exception does not apply to every output path.
How Sensitive works
Sensitive generates traversal code by implementing the RedactableWithMapper trait. Containers are just scaffolding: they get walked recursively until a leaf is reached. Leaves are where things actually happen:
- Unannotated leaves pass through unchanged.
- Annotated leaves (
#[sensitive(Policy)]) are where redaction is applied.
| Field kind | What happens |
|---|---|
Containers (structs/enums deriving Sensitive) |
Traversal walks into them recursively, visiting each field |
Standard leaves (String, primitives, Option, Vec, etc.) |
Built-in RedactableWithMapper implementation that performs no redaction; returned unchanged |
Annotated leaves (#[sensitive(Policy)]) |
The macro generates transformation code that applies the policy, bypassing the normal passthrough |
Explicit passthrough (#[not_sensitive]) |
Skips the RedactableWithMapper requirement entirely; the field is copied as-is with no redaction. Use for types that don't have a built-in implementation |
Why do standard leaves implement RedactableWithMapper?
Every field in a Sensitive type must implement RedactableWithMapper. But redaction should be opt-in: if you don't annotate a field, nothing should happen to it. Standard leaves like String and u32 square this circle by implementing RedactableWithMapper as a no-op. They satisfy the trait bound, but they don't transform anything. You only annotate what you actually want to protect.
Note that this is the traversal machinery only. Leaves do not implement Redactable: calling .redact() on a bare String — or certifying one as redacted output — is a compile error, because nobody declared what redacting it means. Redactable comes from the derives and wrappers.
The following types all have this built-in no-op implementation:
- Scalars:
bool,char,i8..i128,isize,u8..u128,usize,f32,f64,NonZeroI8..NonZeroUsize - Strings:
String,Cow<str> - Containers (delegate to inner values):
Option,Vec,VecDeque, arrays, tuples up to four elements,Box,Arc,Rc,RefCell,Cell,Mutex,RwLock,Result,HashMap,BTreeMap,HashSet,BTreeSet - Other:
Duration,Instant,SystemTime,Ordering,PhantomData; with theip-addressfeature,IpAddr,Ipv4Addr,Ipv6Addr, andSocketAddr - Feature-gated: the
chronofeature adds passthroughs forDateTime<Utc|Local|FixedOffset>,NaiveDateTime,NaiveDate,NaiveTime,Duration,Month, andWeekday; thetimefeature addsOffsetDateTime,PrimitiveDateTime,Date,Time,Duration,UtcOffset,Month, andWeekday; theuuidfeature addsUuid. Theextrasfeature enableschrono,time,uuid, andip-addresstogether
let outer = Outer ;
let redacted = outer.redact;
assert_eq!; // unchanged
assert_eq!; // unchanged
assert_eq!; // unchanged
assert_eq!; // walked and redacted
assert_eq!; // policy applied
What if a field doesn't implement RedactableWithMapper?
If a field type does not implement RedactableWithMapper, you get a compilation error. To fix this:
-
Local types: derive
Sensitiveon the type so it participates in traversal:// now implements RedactableWithMapper -
Foreign types: use
#[not_sensitive]to skip the field:#[not_sensitive]is the simplest escape hatch. Alternatively, the library provides dedicated wrapper types covered in Wrapper types for foreign types.
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 RedactableWithMapper::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 macro recognizes scalars by their bare names (u32, bool, char). Qualified paths like std::primitive::u32 are not recognized and will produce a compile error. Always use the bare name.
How the Sensitive macro processes each field
flowchart TD
F["For each field"] --> A{"Annotated with<br/>#[sensitive(Policy)]?"}
A -- Yes --> T{"Field type?"}
T -- "String-like<br/>(String, Cow, Option<String>, etc.)" --> B["Apply text redaction policy<br/>e.g. Email → al***@example.com"]
T -- "Scalar<br/>(only #[sensitive(Secret)])" --> C["Replace with default<br/>u32 → 0, bool → false, char → *"]
A -- No --> D{"Annotated with<br/>#[not_sensitive]?"}
D -- Yes --> E["Copy as-is<br/>no trait required"]
D -- No --> G{"Implements<br/>RedactableWithMapper?"}
G -- "Yes, container<br/>(derives Sensitive)" --> I["Recurse into its fields"]
G -- "Yes, standard leaf<br/>(String, u32, Option, etc.)" --> J["Passthrough unchanged"]
G -- No --> K["Compile error"]
How SensitiveDisplay works
SensitiveDisplay generates formatting code by implementing the RedactableWithFormatter trait. Unlike Sensitive, which walks every field and produces a redacted copy of the same type, SensitiveDisplay is template-driven: only fields referenced in the display template are formatted. Fields absent from the template are ignored entirely.
It works by reference (no Clone needed) and produces a string:
- Unannotated fields in the template are formatted unchanged.
- Annotated fields (
#[sensitive(Policy)]) have redaction applied before formatting. - Fields not in the template are not formatted at all.
| Field kind | What happens |
|---|---|
Nested types (structs/enums deriving SensitiveDisplay) |
Uses their RedactableWithFormatter to produce a redacted substring |
Standard scalars (String, primitives, Option, Vec, etc.) |
Built-in RedactableWithFormatter implementation; formatted unchanged |
Annotated fields (#[sensitive(Policy)]) |
The macro generates formatting code that applies the policy |
Explicit passthrough (#[not_sensitive]) |
Renders via raw Display (or Debug if {:?}). Skips the RedactableWithFormatter requirement. Use for types without a built-in implementation |
; // Does NOT derive SensitiveDisplay
let err = UserError ;
assert_eq!; // scalars unchanged
let err = AuthFailed ;
assert_eq!; // policy applied
let err = Nested ;
assert_eq!; // nested redaction
Template syntax
The display template comes from one of two sources:
#[error("...")] attribute (thiserror-style):
Doc comment (same syntax as displaydoc, but parsed by the macro itself):
Both support named placeholders ({field_name}), positional placeholders ({0}, {1}), and debug formatting ({field:?}).
Note that {field:?} on an unannotated field renders through redacted-display semantics, not standard Debug: a String prints without quotes or escaping. If you need genuine Debug output (quoting, escape sequences), mark the field #[not_sensitive] — which formats the raw value with real Debug — or pre-format the value yourself.
Positional placeholders must be contiguous starting at 0; {1} without {0} is rejected. Dynamic width or precision, such as {value:.*}, and non-Display/Debug specifiers such as {value:x} are rejected.
Why do scalars implement RedactableWithFormatter?
The same design principle applies: redaction should be opt-in. Every field referenced in a template must implement RedactableWithFormatter, but if you don't annotate a field, nothing should happen to it. Standard scalars like String and u32 implement RedactableWithFormatter as a no-op that formats the value unchanged. You only annotate what you actually want to protect.
The built-in types are the same as for RedactableWithMapper (see the full list). All scalars, strings, containers, and time types have passthrough implementations for both traits.
let event = UserInfo ;
assert_eq!;
What if a field doesn't implement RedactableWithFormatter?
If a template references a field whose type does not implement RedactableWithFormatter, you get a compilation error. To fix this:
-
Local types: derive
SensitiveDisplayon the type so it participates in redacted formatting:// Now DatabaseError implements RedactableWithFormatter -
Foreign types: use
#[not_sensitive]to render via rawDisplayinstead:#[not_sensitive]is the simplest escape hatch. See Wrapper types for foreign types for more patterns.
The #[sensitive(Policy)] attribute in templates
#[sensitive(Policy)] marks a field as sensitive and applies a redaction policy. The behavior is the same as in Sensitive, but the output is formatted into the template string:
#[sensitive(Secret)]on strings: replaces with"[REDACTED]"#[sensitive(Secret)]on scalars: replaces with the default value (0,false,'*')#[sensitive(Policy)]on strings: applies the policy's redaction rules#[sensitive(Policy)]on containers such asOption<String>orVec<String>: applies the policy to each contained string, then formats the redacted container in the template
let event = Login ;
assert_eq!;
How the SensitiveDisplay macro processes each field
flowchart TD
F["For each field<br/>in the template"] --> A{"Annotated with<br/>#[sensitive(Policy)]?"}
A -- Yes --> T{"Field type?"}
T -- "String-like<br/>(String, Cow, Option<String>, etc.)" --> B["Format with redaction policy<br/>e.g. Email → al***@example.com"]
T -- "Scalar<br/>(only #[sensitive(Secret)])" --> C["Format default value<br/>u32 → 0, bool → false, char → *"]
A -- No --> D{"Annotated with<br/>#[not_sensitive]?"}
D -- Yes --> E["Format via raw Display<br/>no trait required"]
D -- No --> G{"Implements<br/>RedactableWithFormatter?"}
G -- "Yes, nested type<br/>(derives SensitiveDisplay)" --> I["Format via fmt_redacted<br/>(redacted substring)"]
G -- "Yes, standard scalar<br/>(String, u32, Option, etc.)" --> J["Format unchanged"]
G -- No --> K["Compile error"]
NotSensitive and NotSensitiveDisplay
Types with no sensitive data still need to participate in the redaction system for two reasons:
-
Composition: every field in a
Sensitivetype must implementRedactableWithMapper, and every field in aSensitiveDisplaytemplate must implementRedactableWithFormatter. Non-sensitive field types need to satisfy these bounds. -
Logging safety: the
SlogRedactedandTracingRedactedmarker traits (see Logging safety) let you enforce that only certified types pass through your logging pipeline. Non-sensitive types need these markers to be loggable alongside sensitive ones.
NotSensitive and NotSensitiveDisplay solve both problems. They generate the required traits as no-op passthroughs and provide full logging integration. The choice between them follows the same sink-driven logic: NotSensitive for the structured path, NotSensitiveDisplay for the string path.
NotSensitive
NotSensitive is for types with no sensitive data that need to work inside Sensitive containers:
use ;
NotSensitive generates:
RedactableWithMapper: no-op passthrough (the type has no sensitive data)Redactable: the derive is an explicit declaration, so the type is certified for.redacted_output()and the other redacted-output extension methodsslog::ValueandSlogRedacted: serializes the value directly as structured JSON, same format asSensitivebut without redaction (whenslogfeature is enabled; requiresSerializeon the type)TracingRedacted: whentracingfeature is enabled
NotSensitiveDisplay
NotSensitiveDisplay is for types with no sensitive data that have a Display impl:
use NotSensitiveDisplay;
/// Retry using backoff
NotSensitiveDisplay generates:
RedactableWithMapper: no-op passthrough (allows use insideSensitivecontainers)Redactable: the derive is an explicit declaration, so the type is certified for the redacted-output extension methodsRedactableWithFormatter: delegates toDisplay::fmt(allows use insideSensitiveDisplaycontainers)ToRedactedOutput: emits theDisplaytext, certifying the type forslog_redacted_display()andtracing_redacted()slog::ValueandSlogRedacted: whenslogfeature is enabledTracingRedacted: whentracingfeature is enabled
This cross-path compatibility makes NotSensitiveDisplay uniquely versatile. It is the only derive that works as a field in both Sensitive and SensitiveDisplay containers.
NotSensitiveDisplay works naturally with displaydoc or similar crates that derive Display:
use NotSensitiveDisplay;
// Now RetryDecision has Display (from displaydoc), RedactableWithFormatter, slog::Value, etc.
What all four derives generate
| Derive | RedactableWithMapper |
Redactable |
RedactableWithFormatter |
ToRedactedOutput |
Debug |
|---|---|---|---|---|---|
Sensitive |
✅ | ✅ | - | - | ✅ (redacted) |
SensitiveDisplay |
- | - | ✅ | ✅ | ✅ (redacted) |
NotSensitive |
✅ | ✅ | - | - | - |
NotSensitiveDisplay |
✅ | ✅ | ✅ | ✅ | - |
Redactable and ToRedactedOutput are the certifications: only types carrying them (or containers of such types) can use .redact(), the redacted-output extension methods, and the slog/tracing certification paths. Bare leaves like String participate in traversal but are never certified.
About Debug:
SensitiveandSensitiveDisplaygenerate a conditional impl: redacted by default, actual values in your crate'scfg(test)builds or when theredactable/testingfeature is enabled. When deriving both, use#[sensitive(dual)]to avoid conflicting impls.NotSensitiveandNotSensitiveDisplaydo not overrideDebug. There is nothing to redact. Add#[derive(Debug)]separately when you need it.
Wrapper types
The library provides format-neutral value wrappers and explicit logging-output wrappers:
SensitiveValue<T, P>- Wraps a value of type
Tand associates it with a redaction policyP - Implements
Debugwith redacted output - Does not implement
Display(prevents accidental raw formatting) - Implements
slog::Value+SlogRedacted(requiresslogfeature) andTracingRedacted(requirestracingfeature) - Provides
.redacted()for the redacted form and.expose()for raw access
- Wraps a value of type
NotSensitiveValue<T>- Wraps a non-sensitive type to satisfy
RedactableWithMapperbounds - Passes the value through unchanged
- Wraps a non-sensitive type to satisfy
NotSensitiveDebug<T>- Owns a value explicitly declared safe to log through
Debug - Implements
ToRedactedOutput, common value traits,inner(), andinto_inner() - Serializes and deserializes as the raw inner value with the
jsonfeature
- Owns a value explicitly declared safe to log through
NotSensitiveDisplay<T>- Owns a value explicitly declared safe to log through
Display - Implements
ToRedactedOutput, common value traits,inner(), andinto_inner() - Serializes and deserializes as the raw inner value with the
jsonfeature
- Owns a value explicitly declared safe to log through
NotSensitiveJson<'_, T> is a borrowed JSON logging view available with the
json feature. NotSensitiveValue<T> deliberately does not implement
ToRedactedOutput: it owns raw application data but does not choose a logging
format.
Choosing a wrapper
Treat explicitly non-sensitive wrappers as exceptional declarations. Most application output can contain sensitive data and should use a policy or a purpose-built redacted projection.
| Need | Use |
|---|---|
| Sensitive leaf with a policy | SensitiveValue<T, P> |
| Sensitive structured output or a restricted public projection | A custom ToRedactedOutput implementation |
Genuinely public value logged with Debug |
NotSensitiveDebug<T> |
Genuinely public value logged with Display |
NotSensitiveDisplay<T> |
| Borrowed value logged as raw JSON | NotSensitiveJson<'_, T> |
| Owned passthrough value with no logging-format decision | NotSensitiveValue<T> |
For example, a public retry count can be owned and logged explicitly:
use ;
let attempts = NotSensitiveDisplay;
assert_eq!;
assert_eq!;
A customer record, token, or handler result that may contain private fields is
not a candidate for these wrappers. Use SensitiveValue<T, P> or implement
ToRedactedOutput for a local projection that exposes only approved fields.
⚠️ With the json feature, NotSensitiveDebug<T> and
NotSensitiveDisplay<T> serialize and deserialize exactly like T. That raw
Serde representation is for normal transport or storage, may expose the entire
value, and is not sanitized logging output.
The same distinction applies to sensitive wrappers: transport keeps the raw value while the logging boundary applies its policy.
use ;
let token = from;
assert_eq!;
assert_eq!;
Migrating a local compatibility wrapper
If a local wrapper exists only to combine ownership, raw Serde, common traits, and an explicit output format, replace it with the matching upstream type:
// Before:
// struct NotSensitiveHandlerOutput<T>(T);
// After, when the complete Debug representation is genuinely safe to log:
use NotSensitiveDebug;
let output = NotSensitiveDebug;
let raw_result = output.into_inner;
Use NotSensitiveDisplay instead when Display is the approved representation.
This migration is incorrect for outputs that may contain sensitive data; keep a
redaction policy or custom projection for those values.
Use cases
Wrapper types exist for two purposes:
Foreign types
You can't derive macros on types defined in other crates, and the orphan rule prevents you from implementing the internal traits they need (RedactableWithMapper, PolicyApplicable) because neither the trait nor the type is local. Wrappers provide those implementations for you. You only need to implement SensitiveWithPolicy<P>, which the orphan rule allows because your policy type is local.
For sensitive foreign types, implement SensitiveWithPolicy<P> with a custom policy (the orphan rule requires the policy type to be local) and wrap with SensitiveValue:
use ;
// Imagine this comes from a payments SDK.
// It exposes accessors but no redaction support.
// The policy type must be local to your crate: that is what satisfies the
// orphan rule for the SensitiveWithPolicy impl on the foreign type.
;
For non-sensitive foreign types, wrap with NotSensitiveValue:
use ;
// (pretend this is from another crate)
Field-level redaction awareness
With #[sensitive(P)] attributes, fields are still bare types at runtime. A String is a String, and nothing stops you from accessing or formatting it unredacted. SensitiveValue<T, P> changes the runtime type itself: the field carries its policy, its Debug shows the redacted form, and Display is deliberately not implemented so accidental formatting won't compile. Each field can be redacted, logged, or inspected independently, without going through the parent container:
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;
Compare with #[sensitive(P)] attributes, where the field is a bare type at runtime:
#[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 your crate's cfg(test) builds or via the redactable/testing feature). This is intentionally stronger than policy-shaped output: derived redacted Debug uses the generic placeholder rather than preserving Email, Token, or other policy-specific shapes. Use .redact() plus serialization or logging when you need policy-shaped output.
⚠️ Things to keep in mind:
NotSensitiveValuehas no role here- It carries no policy and provides no redaction awareness.
- For local non-sensitive types, just use
#[derive(NotSensitive)].
- Serialization is not protected
- Both
#[sensitive(P)]andSensitiveValueserialize to raw values. - This is intentional because APIs, databases, queues, and storage usually need the real data.
- Logging integrations redact before they serialize the log value. Direct
serdeserialization does not. - If you need redacted serialization outside logging, call
.redact(),.redacted_json(), or.to_redacted_output()at the boundary.
- Both
- Wrappers are leaf-only
- Neither wrapper walks nested fields or applies inner
#[sensitive(...)]annotations. - In practice this is not a limitation: types that derive
Sensitivealready implementRedactableWithMapperand don't need wrapping.
- Neither wrapper walks nested fields or applies inner
Integrations
slog
The slog feature enables automatic redaction. Just log your values and they're redacted:
[]
= { = "0.10", = ["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 get it via the derive macro, wrappers via a manual implementation. No explicit conversion needed. Sensitive emits structured JSON; SensitiveDisplay emits the redacted display string.
tracing
For structural values with any tracing subscriber, use the plain tracing
feature and log the redacted Debug form:
[]
= { = "0.10", = ["tracing"] }
use ;
use TracingRedactedDebugExt;
let event = AuthEvent ;
// Redacts a clone before the value reaches the tracing subscriber.
info!;
// Production output: AuthEvent { api_key: "[REDACTED]", user_email: "[REDACTED]", action: "login" }
That exact line is production output. Generated Debug intentionally has a different test-mode branch (cfg(test) or the testing feature); after the tracing adapter redacts the clone, that mode displays AuthEvent { api_key: "***************2345", user_email: "al***@example.com", action: "login" }.
For typed structured logging, use the valuable integration. Upstream tracing
requires RUSTFLAGS="--cfg tracing_unstable" for tracing::field::valuable,
and the field expression must pass a reference through that adapter:
[]
= { = "0.10", = ["tracing-valuable"] }
use ;
use TracingValuableExt;
let event = AuthEvent ;
let redacted = event.tracing_redacted_valuable;
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, but RedactedValuable<T> is not itself a tracing field
value. .tracing_redacted_valuable() redacts first; tracing::field::valuable
adapts the binding for subscribers that support valuable.
For flat display values (without valuable):
use ;
use TracingRedactedExt;
let api_key: = from;
let user_email: = from;
info!;
// Logged: api_key="***************2345" user_email="al***@example.com" action="login"
The display path also works for SensitiveDisplay, NotSensitiveDisplay, and
other values that implement ToRedactedOutput.
Logging safety
For most use cases, the slog and tracing integrations handle safety automatically. This section covers how to enforce redaction at compile time and how to build custom logging pipelines.
Enforcing redaction at compile time
SlogRedacted and TracingRedacted are marker traits for values that have
redactable sink integrations. All four derive macros implement them
automatically when the corresponding feature is enabled, as does
SensitiveValue<T, P>. A bound is only half of the contract: your macro must
also call the redacting adapter instead of passing raw values to the sink.
For slog, use SlogRedacted with slog::Value and pass the value to slog's
field API:
use SlogRedacted;
// ✅ 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 structural tracing fields, use the extension trait as the compile-time gate:
use TracingRedactedDebugExt;
ToRedactedOutput for custom pipelines
If you're not using slog or tracing, ToRedactedOutput is the single logging-safe bound. It produces a RedactedOutput: either Text(String) or Json(serde_json::Value) (requires json feature). This trait is deliberately narrower than RedactableWithFormatter: raw String and scalar values can format inside a redacted template, but they do not satisfy ToRedactedOutput unless you explicitly wrap them as non-sensitive.
The certification methods follow the same rule through the traits you already know: .redacted_output(), .redacted_json(), and .slog_redacted_json() require Redactable, and .slog_redacted_display() requires ToRedactedOutput — both implemented only by the derives, the wrapper types, and containers of them, never by raw passthrough leaves. A bare String or Vec<String> cannot be redacted or certified as redacted output; the compile error points at the derives and wrappers to use instead.
| Situation | Method | Returns |
|---|---|---|
| Structured container → redacted text | .redacted_output() |
RedactedOutput::Text |
| Structured container → redacted JSON | .redacted_json() (requires json feature) |
RedactedOutput::Json |
SensitiveDisplay type → redacted text |
.to_redacted_output() or pass the value to a ToRedactedOutput bound |
RedactedOutput::Text |
| Non-sensitive (delegate to framework) | .not_sensitive() |
NotSensitive<&Self> |
| Non-sensitive (explicit Display) | .not_sensitive_display() |
NotSensitiveDisplay<&T> |
| Non-sensitive (explicit Debug) | .not_sensitive_debug() |
NotSensitiveDebug<&T> |
| Non-sensitive (explicit JSON) | .not_sensitive_json() (requires json feature) |
NotSensitiveJson<&T> |
.redacted_json() always produces RedactedOutput::Json. If serialization of the redacted value fails, it returns the fixed JSON string "[REDACTED]" and never includes serializer error text or user data.
Choosing what to use
This section brings together the decisions covered throughout the README into a single reference.
Which derive macro?
flowchart TD
A{"Does the type<br/>contain sensitive data?"} -- Yes --> B{"What output?"}
A -- No --> C{"Does it need to satisfy<br/>redaction trait bounds?"}
B -- "Structured value<br/>(JSON, slog, valuable)" --> D["derive Sensitive"]
B -- "Formatted string<br/>(errors, display, flat logs)" --> E["derive SensitiveDisplay"]
C -- "Yes, local type" --> F{"Which container path?"}
C -- "Yes, foreign type" --> G["NotSensitiveValue<T> wrapper<br/>or #[not_sensitive] attribute"]
C -- No --> H["No derive needed"]
F -- "Sensitive containers only" --> I["derive NotSensitive"]
F -- "SensitiveDisplay containers<br/>or both" --> J["derive NotSensitiveDisplay"]
How to handle each field
flowchart TD
A{"Is the field<br/>sensitive?"} -- Yes --> B{"Is the type<br/>foreign?"}
A -- No --> C{"Is the type<br/>foreign?"}
B -- Yes --> D["SensitiveValue<T, P><br/>+ impl SensitiveWithPolicy"]
B -- No --> E{"Need field-level<br/>Debug/logging safety?"}
E -- Yes --> F["SensitiveValue<T, P> wrapper"]
E -- "No (most common)" --> G["#[sensitive(Policy)] attribute"]
C -- Yes --> H["#[not_sensitive] attribute<br/>or NotSensitiveValue<T>"]
C -- No --> I["No annotation needed"]
How to log safely
| Situation | Use |
|---|---|
| slog | Log containers directly: slog::info!(logger, "msg"; "key" => &value) |
| tracing (structural Debug) | .tracing_redacted_debug() on Redactable + Clone + Debug values |
| tracing (valuable structured) | let redacted = value.tracing_redacted_valuable(); tracing::field::valuable(&redacted) with RUSTFLAGS="--cfg tracing_unstable" |
| tracing (flat display) | .tracing_redacted() on ToRedactedOutput values |
| Custom logging pipeline | ToRedactedOutput trait bound (details) |
| Compile-time enforcement | SlogRedacted, TracingRedactedDebugExt, or ToRedactedOutput bounds in macros (details) |
Reference
Supported types
#[sensitive(Policy)] works on string-like types: String, Cow<'_, str>, and wrappers around them like Option<String>. Policy redaction by reference for Cow<'_, str> returns an owned Cow<'static, str> so formatted redacted output never borrows from the original. &str is not supported for Sensitive; use owned strings or Cow.
#[sensitive(Secret)] also works on scalars: integers are replaced with 0, floats with 0.0, bool with false, char with '*'. NonZero* integer types cannot be policy-annotated because redaction may need to produce zero; leave them unannotated or wrap a different representation.
Containers (Option, Vec, VecDeque, arrays, tuples up to four elements, maps, sets, and pointer/cell/lock wrappers) are walked automatically. Policy annotations apply through supported policy containers such as Option, Vec, VecDeque, arrays, Result, maps, and sets. Map keys are formatted with Debug and are not redacted.
IP address types (IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr) are available with the ip-address feature. Unannotated IP fields pass through unchanged. #[sensitive(IpAddress)] applies to bare IP fields only; it does not currently apply through Option<IpAddr>, Vec<IpAddr>, or other containers. For IP values inside containers, wrap each value with SensitiveValue<IpAddr, IpAddress> or the matching concrete IP type. IPv4 addresses keep only the last octet (0.0.0.77), IPv6 addresses keep only the last 16-bit segment, and IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) are redacted with the IPv4 rule so dual-stack listeners don't leak extra octets.
serde_json::Value (requires json feature) is treated as an opaque traversal leaf that fully redacts to Value::String("[REDACTED]"), even when unannotated, during .redact() and adapters that invoke it. Direct derive-generated Debug remains annotation-driven.
For the full list of types with built-in passthrough implementations, see Why do standard leaves implement RedactableWithMapper?.
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.
Empty strings redact to the placeholder. Policies that would otherwise keep or mask part of the input return "[REDACTED]" for an empty string so redaction is visible.
Short values are fully masked. Keep-based policies (Token, CreditCard, Pii, etc.) fail closed: when the value is at or below the keep window (e.g. a 4-character token under Token's keep-last-4), every character is masked instead of revealed. The Email policy applies the same rule to the local part.
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.
Sensitivity attributes are per-field. Placing #[sensitive(...)] or #[not_sensitive] on an enum variant is a compile error; annotate the variant's fields instead.
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 ;
;