Skip to content

Pragmatic.Redaction

What must not reach a log or an audit trail, removed in two complementary ways: by shape (the personal-data patterns worth recognising in free text, in one place) and by declaration (the members a type marked [NotLogged] or [PersonalData]).

Two subsystems that each carry their own pattern set drift apart: one lets a national identifier (123-45-6789) or a card-verification value (cvv: 123) through in plaintext while the other catches both. One set, shared by the logging pipeline and the audit trail, keeps them equal.

It matters most for the audit trail. A log rotates; the audit trail is append-only and kept for years, so its redaction floor must never be the lower of the two.

var safe = PersonalDataRedactor.Redact(text);

Covers e-mail, IBAN, long digit runs (payment cards, phone and account numbers), national identifiers, card-verification values, bearer tokens, and secrets assigned by name (password=, api_key:).

Pattern matching cannot recognise a name, an address, or a sentence about someone’s health. A caller that puts a personal value into free text is relying on a net with holes. The point is that the obvious shapes do not get through, not that nothing does.

A date is personal data only in context: a birth date is, locked until 2026-08-01 is not, and no pattern tells them apart. Redacting every date would empty the one field that explains why an entry exists, which is how a redactor stops being used at all.

A pattern cannot see what has no shape: an internal identifier, a pricing coefficient, a token that looks like any other string. For those the type says it: [NotLogged] (in Pragmatic.Abstractions) or [PersonalData], and the source generator emits an IRedactionMap per type, with the path to every marked member, owned records and collections included. DeclaredRedactor walks those paths; nothing reflects over the payload.

The value is serialized before it is masked, and it is written in camelCase, like every other complex value the JSON log providers write. Under Native AOT the serialization needs metadata: when the assembly emits a generated JSON context (PublishAot or PragmaticGenerateJsonContext), every type in its map is in that context, and the map hands it to the redactor. A type the generator cannot describe (an object member, a property without a public setter) has no metadata. Its value is then written as the mask, whole, and counted in DeclaredRedactor.ValuesWithoutMetadata; the entry is never lost and nothing goes out in clear.

In a Pragmatic host this is wired for you: when any map exists the generated startup calls AddDeclaredRedaction(), which wraps the ILoggerFactory in a RedactingLoggerFactory, so every provider, including one a test adds later, receives the masked value.

The two are governed differently on purpose. Pattern redaction is a heuristic, with false positives, and sits behind the logging pipeline’s EnableRedaction setting. Declared redaction is not optional and has no environment qualifier: “never in the logs” does not mean “except on my machine”.

Pragmatic.Audit uses PersonalDataRedactor. Pragmatic.Logging takes its e-mail and IBAN patterns from here and still carries the rest of its set on its own: national identifiers, cards, and credentials (AWS keys, GitHub tokens, Stripe keys, private keys), which are a different problem from personal data and are not in scope here.

Preview within 1.0.0-alpha: the personal-data shapes recognised in free text, used by the logging and audit modules and by the composed host. See the roadmap.

  • .NET 10.0+

Part of the Pragmatic.Design ecosystem. See Licensing. Pragmatic.Redaction is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).