Redaction and exclusion are not two settings of the same feature. Redaction keeps the fact that a field changed and withholds the value. Exclusion leaves no trace the field was ever touched. An audit trail that conflates the two is lying by omission.
We ran into this while building model-audit, our open-source change-tracking library. The pressure is real: someone asks you to keep a password hash out of the log, and the easy fix is to drop the field. Now the trail says the record was not modified. Six months later, in an incident review, that silence reads as evidence.
So we split them into two different calls. Register a field with redact and the change gets recorded by field name, with the before and after values withheld. Register it with exclude and the field drops out entirely, so the trail never claims it was involved.
The mechanics that make redaction safe are small, and worth stealing.
The placeholder is a str subclass that reads "[redacted]", so every consumer that serializes records to JSON, logs, or a diff view keeps working untouched. Nothing downstream needs special-casing. But because it is a singleton, an identity check still tells you a value was deliberately withheld rather than genuinely empty.
Redaction also preserves the shape of the change. If a field had no prior value, the sentinel for absence survives redaction, so an addition stays an addition instead of a mysterious edit from one hidden value to another. Lose that distinction and you cannot tell account creation from account modification.
Matching is a substring test on normalized field names against a sensitive-field set. Deliberately blunt, deliberately over-eager. We left email out of the defaults, because for a lot of products the email address is the business record and not a secret. And we added an allow list specifically to release false positives, because a blunt matcher without an escape hatch gets disabled wholesale by the first team it annoys. Noisy fields (the ones that change on every save and tell you nothing) are excluded by default, with a flag to keep them when you actually want the churn.
One decision we deliberately did not make: where the trail lives. The library records changes and hands each record to subscribed receivers. Database, log pipeline, append-only store, retention window, all of that is the project's call, shaped by its own compliance obligations. A library that picks your storage has picked your retention policy too, and it has no business doing that.
The general rule we keep coming back to: an audit trail's value is in what it still proves after you remove the sensitive parts. Removing the value is a tool. Removing the evidence is a bug.
For those of you running audit logs under a retention policy, the choice between redacting at write time and redacting at read time is probably the next thing to settle.