Redactable
redactable marks sensitive fields in Rust structs and enums and produces
redacted output for logging and telemetry. It is not tied to a logging
framework.
Rust examples shown as runnable are compiled by the repository doctest gate. Blocks
marked ignore are deliberately incomplete sketches or require an external runtime
such as a configured logger; blocks marked compile_fail document rejected usage.
Table of Contents
- Getting started
- Design principles
- How Sensitive works
- How SensitiveDisplay works
- NotSensitive and NotSensitiveDisplay
- Wrapper types
- Integrations
- Logging safety
- Reference
Getting started
There are three derive macros for types with sensitive data. Use Sensitive for
structured redaction, SensitiveDisplay for formatted redaction, or
SensitiveDual when the same type needs both paths.
Use Sensitive when you need a structured redacted value. .redact()
returns the same type with its sensitive fields transformed. The result can be
serialized, passed to slog, or inspected through valuable.
Use SensitiveDisplay when you need formatted redacted text.
.redacted_display() returns a displayable view for errors, flat log lines,
and other text output.
Quick examples
The runnable structured example uses Serde directly, so declare it alongside
redactable. Version 0.11 requires Rust 1.97 or later.
[]
= "0.11"
= { = "1", = ["derive"] }
Structured (Sensitive), with a redacted copy:
use ;
let user = User ;
let redacted = user.clone.redact;
assert_eq!;
assert_eq!;
String (SensitiveDisplay), logged as text:
use ;
let err = InvalidCredentials ;
assert_eq!;
What each derive generates
| Derive | Use | Structured | Formatted | Debug |
|---|---|---|---|---|
Sensitive |
Structured values | Redactable |
- | Redacted |
SensitiveDisplay |
Text output | - | ToRedactedOutput |
Redacted |
SensitiveDual |
Both paths | Redactable |
ToRedactedOutput |
Redacted |
NotSensitive |
Certified non-sensitive structured values | Redactable |
- | Not generated |
NotSensitiveDisplay |
Certified non-sensitive values on both paths | Redactable |
ToRedactedOutput |
Not generated |
Security warning: all three sensitive derives generate a conditional
Debugimpl that reveals actual field values in your crate'scfg(test)builds or when theredactable/testingfeature is enabled. Never enable that feature in a production logging build. All field types must implementDebug.
The sensitive derives also generate the logging integrations enabled by the
slog and tracing features. See Integrations for their sink
behavior and Logging safety for owned and borrowed adapters.
SensitiveDual replaces the 0.10 combination of Sensitive,
SensitiveDisplay, and #[sensitive(dual)]. It generates both paths in one
derive. The legacy form now produces a migration diagnostic.
Design principles
The library follows three principles:
- Redaction is opt-in. Unannotated fields pass through unchanged.
- Traversal is automatic. Supported containers delegate recursively to their contents.
- Both output paths use the same annotations.
#[sensitive(Policy)]applies a policy;#[not_sensitive]declares an explicit passthrough.
Threat model
Redactable protects output only when it passes through a redacted API or a generated logging integration. Raw field access, direct Serde serialization, and explicit accessors can still expose the original value. Policies may also retain approved fragments such as an email domain or token suffix.
The testing feature exposes raw generated Debug, and derived containers
that implement Drop are unsupported. Borrowed adapters clone the value and
inherit Clone panics. See Logging safety for the owned and
borrowed adapter contracts.
serde_json::Value is the main traversal exception. With the json feature,
an unannotated value redacts to "[REDACTED]" during .redact() and adapters
that invoke it. Generated Debug remains annotation-driven.
How Sensitive works
Sensitive implements RedactableWithMapper. Containers delegate recursively
until traversal reaches a leaf:
- 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 |
Ordinary leaves (String, primitives, etc.) |
Built-in RedactableWithMapper implementation that performs no redaction; returned unchanged |
Supported containers (Option, Vec, maps, sets, pointers/cells, etc.) |
Delegate recursively to their contained values |
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 |
use ;
Why do standard leaves implement RedactableWithMapper?
Every field in a Sensitive type must implement RedactableWithMapper.
Standard leaves such as String and u32 implement it as a no-op, so
unannotated data passes through unchanged.
This is traversal machinery, not output certification. Bare leaves do not
implement Redactable, so calling .redact() on a String is a compile error.
Certification comes from derives and explicit wrappers.
A summary of built-in leaves and containers is in Supported types.
use ;
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:use Sensitive; // now implements RedactableWithMapper -
Foreign types: use
#[not_sensitive]to skip the field. This sketch uses a placeholder external crate and is intentionally incomplete:#[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
#[sensitive(Policy)] marks a leaf as sensitive. The derive applies the policy
instead of the normal RedactableWithMapper 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
use ;
#[sensitive(Secret)] accepts both bare primitive names such as u32 and qualified standard-library paths such as std::primitive::u32.
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 becomes al***@example.com"]
T -- "Scalar<br/>(only #[sensitive(Secret)])" --> C["Replace with default<br/>u32 becomes 0, bool becomes false"]
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, ordinary leaf<br/>(String, u32, etc.)" --> J["Passthrough unchanged"]
G -- "Yes, supported container<br/>(Option, Vec, map, etc.)" --> I
G -- No --> K["Compile error"]
Types that implement Drop
Sensitive consumes self and moves its fields into a redacted value of the
same type. Container types that implement Drop are unsupported, including
Copy-only shapes that happen to compile: .redact() drops the consumed original
and later drops the replacement, which is not a supported container lifecycle.
This limitation also applies to SensitiveDual. A non-Copy field usually
makes the unsupported shape fail earlier with E0509.
The restriction is on the derived container itself. A type that does not
implement Drop can still derive Sensitive when its fields have their own
drop behavior, provided those fields satisfy the usual traversal bounds.
How SensitiveDisplay works
SensitiveDisplay implements RedactableWithFormatter. It is template-driven:
only fields referenced in the display template are formatted. Sensitive
instead walks every field and produces a redacted value of the same type.
It formats by reference and produces a string. The generated text/secret route
does not require Clone; individual policy projections can add documented
bounds (IP-policy maps currently clone allowed keys and hashers):
- 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 |
use ;
; // 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):
use SensitiveDisplay;
Doc comment (same syntax as displaydoc, but parsed by the macro itself):
use SensitiveDisplay;
Both support named placeholders ({field_name}), positional placeholders ({0}, {1}), and debug formatting ({field:?}).
Note that {field:?} on an unannotated field uses redacted-display semantics,
not standard Debug: a String prints without quotes or escaping. For genuine
Debug output, mark the field #[not_sensitive] or pre-format the value.
Positional placeholders must be contiguous from 0; {1} without {0} is
rejected. Dynamic width or precision, such as {value:.*}, and non-Display or
Debug specifiers, such as {value:x}, are also rejected.
Why do scalars implement RedactableWithFormatter?
Every field referenced in a template must implement
RedactableWithFormatter. Standard scalars such as String and u32
implement it as a no-op, so unannotated values format unchanged.
The built-in types match RedactableWithMapper. Ordinary scalar, string, and
time leaves pass through; supported containers delegate recursively.
use ;
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:use SensitiveDisplay; // Now DatabaseError implements RedactableWithFormatter -
Foreign types: use
#[not_sensitive]to render via rawDisplayinstead. This sketch uses a placeholder external crate and is intentionally incomplete:#[not_sensitive]is the simplest escape hatch. See Wrapper types for foreign types for more patterns.
The #[sensitive(Policy)] attribute in templates
#[sensitive(Policy)] has the same policy behavior as Sensitive, but formats
the result into the template:
#[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
use ;
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 becomes al***@example.com"]
T -- "Scalar<br/>(only #[sensitive(Secret)])" --> C["Format default value<br/>u32 becomes 0, bool becomes false"]
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: non-sensitive field types still need to satisfy the structured or formatted traversal bound of their container.
-
Logging safety: non-sensitive types need explicit certification to pass the same logging bounds as sensitive values.
NotSensitive certifies the structured path. NotSensitiveDisplay certifies
both the structured and formatted paths. Both generate no-op traversal and the
enabled logging integrations.
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 explicitly non-sensitive value directly as structured JSON (whenslogis 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 lets NotSensitiveDisplay work as a field in
both Sensitive and SensitiveDisplay containers. SensitiveDual is the
sensitive cross-path derive when both behaviors are required.
NotSensitiveDisplay works naturally with displaydoc or similar crates that derive Display:
This optional example requires a direct displaydoc dependency and is
intentionally not part of the standalone doctest set:
use NotSensitiveDisplay;
// Now RetryDecision has Display (from displaydoc), RedactableWithFormatter, slog::Value, etc.
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()
- 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()
- 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> |
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.
Sensitive wrappers follow the same rule: 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:
The following is a migration sketch with application-specific values omitted:
// 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
Types from other crates cannot use your derives, and the orphan rule prevents
you from implementing redactable's traversal traits for them. Wrappers provide
those implementations. A local policy type can implement
SensitiveWithPolicy<P> for the foreign value.
For a sensitive foreign type, define a local policy,
implement SensitiveWithPolicy<P>, and use 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)], a field keeps its original runtime type and can still be
accessed or formatted without redaction. SensitiveValue<T, P> carries the
policy in the runtime type, provides redacted Debug, and deliberately omits
Display.
Normally choose exactly one policy form: annotate a bare field with
#[sensitive(P)], or use an unannotated SensitiveValue<T, P>. If a direct
annotation is combined with a wrapper in a display shape that compiles, the
wrapper's own policy is authoritative.
This logging sketch omits the surrounding application logger configuration:
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 raw value | ✅ Shows policy-redacted value |
| Serialization | Shows raw value | Shows raw value |
| slog/tracing safety | ✅ Via container | ✅ Direct |
The attribute affects output generated for the containing type, not direct
formatting of the field. Sensitive's generated Debug uses the generic
[REDACTED] placeholder. SensitiveDisplay and the display-selected Debug
generated by SensitiveDual use the declared template, so policy annotations
may preserve shaped fragments such as an email domain or token suffix. These
redacted implementations are disabled in your crate's cfg(test) builds or via
the redactable/testing feature.
Both forms serialize raw values. Use .redact(), .redacted_json(), or
.to_redacted_output() when the serialized boundary must be redacted.
SensitiveValue is a leaf wrapper and does not walk nested field annotations.
Local structured types should derive Sensitive instead.
Integrations
slog
The slog feature enables automatic redaction. Just log your values and they're redacted:
[]
= { = "0.11", = ["slog"] }
= { = "1", = ["derive"] }
= "2.8"
Structured slog output relies on nested-value support throughout the drain
stack. When using drains such as slog-async or slog-json, enable each
drain crate's nested-values feature as well. Enabling redactable/slog
enables the feature on slog itself, but not on separate drain crates.
Containers: the Sensitive derive generates slog::Value automatically:
use ;
use Serialize;
let event = PaymentEvent ;
// Just log it - slog::Value impl handles redaction automatically
let logger = root;
info!;
// Borrowed generated output: "[REDACTED]"
Leaf wrappers: SensitiveValue<T, P> also implements slog::Value:
use ;
let api_token: = from;
// Also automatic - SensitiveValue has its own slog::Value impl
let logger = root;
info!;
// Logged: "*********-key"
Both work because they implement slog::Value. Containers get it via the
derive macro, wrappers via a manual implementation. Borrowed Sensitive and
SensitiveDual values fail closed to "[REDACTED]"; consume an owned value
with .slog_redacted_json() when structured JSON is required.
tracing
For structural values with any tracing subscriber, use the plain tracing
feature and log the redacted Debug form:
[]
= { = "0.11", = ["tracing"] }
= "0.1"
The tracing sink examples are exercised by the stable and unstable tracing integration tests in the reusable verification workflow:
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. In cfg(test) or with the testing
feature, generated Debug shows the policy-shaped fields after the tracing
adapter redacts its clone:
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.11", = ["tracing-valuable"] }
= "0.1"
= { = "0.1", = ["derive"] }
This example requires RUSTFLAGS="--cfg tracing_unstable"; the reusable CI
gate runs it under that configuration:
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 TracingRedactedValue<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, SensitiveDual,
NotSensitiveDisplay, and other values that implement ToRedactedOutput.
Logging safety
The slog and tracing integrations handle the common sink
paths. Marker traits and ToRedactedOutput enforce the same boundary in custom
logging code.
Enforcing redaction at compile time
SlogRedacted and TracingRedacted are marker traits for values with
logging-safe sink integrations. All five derive macros implement them
automatically when the corresponding feature is enabled, as does
SensitiveValue<T, P>. Calling .not_sensitive() is an explicit declaration
that certifies its wrapper for TracingRedacted and, when the wrapped value or
reference implements slog::Value, for SlogRedacted; the raw value, including
a raw String, remains uncertified. A bound is only half of the contract: your
macro must also call the redacting adapter or pass the explicitly certified
wrapper instead of passing raw values to the sink.
For slog, use SlogRedacted with slog::Value and pass the value to slog's
field API:
This macro sketch includes application values and a deliberately rejected raw field call, so it is verified by the slog integration and compile-fail suites:
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:
This macro sketch requires a live tracing sink and is verified by the tracing integration suite:
use TracingRedactedDebugExt;
ToRedactedOutput for custom pipelines
For custom logging, require ToRedactedOutput. It produces
RedactedOutput::Text(String) or, with the json feature,
RedactedOutput::Json(serde_json::Value). Raw strings and scalars can format
inside a redacted template, but they are not certified output unless explicitly
wrapped as non-sensitive.
Standard containers do not gain output certification from their elements. A
bare String or Vec<String> cannot be certified as redacted output.
| Method | Required bounds |
|---|---|
.redacted_output() |
Redactable + Clone + Debug |
.redacted_json() |
Redactable + Clone + Serialize |
.into_redacted_output() |
Redactable + Debug |
.into_redacted_json() |
Redactable + Serialize |
.slog_redacted_json() |
Redactable + Serialize |
.slog_redacted_display() |
RedactableWithFormatter + ToRedactedOutput |
.tracing_redacted() |
ToRedactedOutput |
.tracing_redacted_debug() |
Redactable + Clone + Debug |
.tracing_redacted_valuable() |
Redactable + Clone + Valuable |
.into_tracing_redacted_debug() |
Redactable + Debug |
.into_tracing_redacted_valuable() |
Redactable + Valuable |
Borrowed adapters preserve the original by cloning it and inherit every Clone
panic. A traversed RefCell with a live mutable borrow is one concrete case.
Use an into_* adapter when ownership is available.
Consuming adapters call .redact() on the owned value and accept every
Redactable shape. Traversal may still clone shared Arc or Rc referents and
map or set hashers. A live RefCell mutable borrow behind shared ownership can
therefore still panic. Prefer Box when the logged value has unique ownership.
.redacted_json() always produces RedactedOutput::Json. On serialization
failure, it returns the fixed JSON string "[REDACTED]"; serializer errors and
input data are never included.
Reference
Supported types
#[sensitive(Policy)] supports String, Cow<'_, str>, and wrappers such as
Option<String>. Borrowed redaction of Cow<'_, str> returns an owned
Cow<'static, str>. Sensitive does not support &str; use an owned string or
Cow.
#[sensitive(Secret)] supports scalars: integers become 0, floats become
0.0, bool becomes false, and char becomes '*'. NonZero* integers
cannot be policy-annotated because redaction may need to produce zero.
Supported containers are walked automatically. Policy annotations recurse
through options, sequences, arrays, results, maps, and sets. Map keys are not
redacted. Generated formatting invokes each key's compact or alternate Debug
implementation exactly once.
Built-in passthrough support covers:
- scalars,
String, andCow<str> Option,Vec,VecDeque, arrays, tuples up to four elements,Box,Arc,Rc,RefCell,Cell,Mutex,RwLock,Result, maps, and setsDuration,Instant,SystemTime,Ordering, andPhantomDatachrono,time,Uuid, and IP address types through their corresponding features;extrasenables all four groups
Consuming .redact() on a poisoned Mutex or RwLock recovers and redacts
the inner value, then returns a new unpoisoned lock. The result is a logging
projection and does not prove that the original protected value satisfied its
invariants when the lock became poisoned.
The ip-address feature supports IpAddr, Ipv4Addr, Ipv6Addr, and
SocketAddr. Unannotated IP fields pass through unchanged.
#[sensitive(IpAddress)] accepts a typed IP only as a bare field, including a
bare type alias. Inside containers, wrap each typed value in
SensitiveValue<_, IpAddress>. IP policies can recurse through text values.
IP-policy maps preserve their keys and accept only known-safe non-text scalar
key types. Formatting clones allowed keys, and HashMap requires a cloneable
hasher. IPv4 output keeps the last octet; IPv6 output keeps the last 16-bit
segment. IPv4-mapped IPv6 uses the IPv4 rule. SocketAddr preserves its port.
With the json feature, serde_json::Value is an opaque traversal leaf. It
redacts to Value::String("[REDACTED]") during .redact() and adapters that
invoke it, even when unannotated. Generated Debug remains annotation-driven.
The API trait implementation lists are authoritative for individual types and feature gates.
Advanced derive options
Most types need no #[redactable(...)] field option. The derive macros expose
three narrow overrides for shapes that procedural macros cannot infer on stable
Rust:
recursivesuppresses a cyclic inferred bound on a recursive field.generated_formattingselects the library formatter for an alias-hidden built-in container.legacy_formattingselects a customPolicyApplicableRefprojection.
The options apply only to fields, and the formatting options require
#[sensitive(Policy)] on the same field. legacy_formatting inherits the
custom projection's Clone and RefCell behavior; the generated formatter
borrows map keys and renders a conflicting nested RefCell borrow as
<borrowed>. Custom PolicyApplicableRef leaves used directly by
SensitiveDisplay must also implement the formatting companion described in
the SensitiveDisplay API documentation.
Direct generic calls to the legacy PolicyApplicable methods require
P::Kind: RecursivePolicyKind. Use the kind-aware apply_policy and
apply_policy_ref free functions when P may be an IP policy. The borrowed
free function uses ordinary RefCell borrowing and can panic on a conflicting
mutable borrow; generated formatting renders <borrowed> instead.
Precedence and edge cases
Policy fields: strings and their containers accept text policies. Scalars
accept only Secret. Use SensitiveValue<T, Policy> for custom types.
Empty strings: policies return "[REDACTED]" so redaction remains visible.
Short values: keep-based policies fully mask values at or below the keep
window. Email applies the same rule to its local part.
Unannotated containers: traversal still applies annotations found inside a
nested Sensitive type.
Sensitivity attributes are per-field. Placing #[sensitive(...)] or #[not_sensitive] on an enum variant is a compile error; annotate the variant's fields instead.
Code-generation helpers are per-field. Placing #[not_sensitive] or
#[redactable(...)] on a struct/enum container, or placing
#[redactable(...)] on an enum variant, is a compile error.
Sets can collapse: redacted elements are collected back into a set. If
several values become equal, the result shrinks. Use a Vec when cardinality
must be preserved.
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 | 0.0.0.100 (last IPv4 octet) |
BlockchainAddress |
Wallet addresses | ************abcdef (last 6) |
Custom policies
use ;
;