Skip to content

Serilog ​

Tayra integrates with Serilog to automatically redact [PersonalData]-annotated properties when objects are logged via Serilog's {@Object} destructuring syntax. This prevents PII from leaking into log files, consoles, or centralized logging platforms.

Prerequisites

Tayra.Serilog requires Serilog 4.x or later.

Install ​

shell
dotnet add package Tayra.Serilog
powershell
Install-Package Tayra.Serilog

Setup ​

Standalone (no DI) ​

cs
var metadataCache = new PersonalDataMetadataCache();

Log.Logger = new LoggerConfiguration()
    .Destructure.WithTayra(metadataCache)
    .WriteTo.Console()
    .CreateLogger();
anchor

With Dependency Injection ​

cs
services.AddTayra(opts => opts.LicenseKey = licenseKey); // defaults to InMemoryKeyStore; use PostgreSQL, Vault, etc. in production

services.AddTayraSerilog(opts =>
{
    opts.UsePartialRedaction = true;
});
anchor

AddTayraSerilog() registers TayraDestructuringPolicy as a singleton and as a Serilog IDestructuringPolicy, so Serilog's standard DI setup attaches it: ReadFrom.Services(services) (from Serilog.AspNetCore or Serilog.Extensions.Hosting) adds every registered policy to the logger.

cs
// Serilog.AspNetCore / Serilog.Extensions.Hosting: ReadFrom.Services picks up the policy
// AddTayraSerilog() registered, along with any other Serilog services in the container.
builder.Host.UseSerilog((context, services, logger) => logger
    .ReadFrom.Services(services)
    .WriteTo.Console());
anchor

If you configure the logger from the service provider without ReadFrom.Services, attach the policy explicitly:

cs
builder.Host.UseSerilog((context, services, logger) => logger
    .Destructure.WithTayra(services)
    .WriteTo.Console());
anchor

Either way the logger must be built from the service provider. A logger built on its own (new LoggerConfiguration()...CreateLogger() at startup) never sees the container; use the standalone setup for that.

How It Works ​

When Serilog encounters {@Object} in a log message template, it calls Tayra's TayraDestructuringPolicy. The policy inspects the object's type metadata and redacts any properties annotated with Tayra PII attributes:

cs
public class User
{
    [DataSubjectId]
    public string UserId { get; set; } = "";

    [PersonalData]
    public string Name { get; set; } = "";

    [PersonalData]
    public string Email { get; set; } = "";

    public string Role { get; set; } = ""; // Not PII
}
anchor
cs
var user = new User
{
    UserId = "u1", Name = "Alice",
    Email = "alice@test.com", Role = "Admin"
};
Log.Information("User: {@User}", user);
anchor

Without Tayra:

User: {"UserId": "u1", "Name": "Alice", "Email": "alice@test.com", "Role": "Admin"}

With Tayra:

User: {"UserId": "u1", "Name": "[REDACTED]", "Email": "[REDACTED]", "Role": "Admin"}

Objects without any PII attributes are passed through to Serilog's default destructuring unchanged - zero overhead for non-PII types.

Options ​

cs
services.AddTayraSerilog(opts =>
{
    opts.RedactedPlaceholder = "[REDACTED]";  // default
    opts.UsePartialRedaction = true;
    opts.IncludeFieldKindHint = true;
    opts.RedactDataSubjectId = false;         // default
});
anchor
PropertyTypeDefaultDescription
RedactedPlaceholderstring"[REDACTED]"The placeholder string for redacted values
UsePartialRedactionboolfalseUse the field's masking strategy to produce partially-masked values (e.g. "Al***")
IncludeFieldKindHintboolfalseAppend the field name to the placeholder (e.g. "[REDACTED:Email]")
RedactDataSubjectIdboolfalseAlso redact properties marked with [DataSubjectId]

Supported Field Kinds ​

The policy handles all Tayra field kinds:

AttributeBehavior
[PersonalData] (string)Replaced with placeholder (or partial redaction / custom ReplacementValue)
[PersonalData] on List<string>Each element redacted, collection length preserved
[PersonalData] on a non-string memberReplaced with placeholder
[DeepPersonalData]Recursively destructured with PII scrubbing
[DeepPersonalData] on collectionsEach element recursively destructured
[DataSubjectId]Passed through by default; redacted when RedactDataSubjectId = true

Partial Redaction ​

When UsePartialRedaction is enabled, fields with a redaction strategy produce masked values instead of the full placeholder:

cs
public class Customer
{
    [DataSubjectId]
    public Guid Id { get; set; }

    [PersonalData(Masking = MaskingStrategies.MaskAfter, MaskingParameter = 2)]
    public string Name { get; set; } = "";

    [PersonalData(Masking = MaskingStrategies.MaskEmailDomain)]
    public string Email { get; set; } = "";
}

// Logs: {"Name": "Al***", "Email": "alice@****.***"}
anchor

Custom ReplacementValue ​

Fields with an explicit ReplacementValue always use that value, regardless of other options:

cs
[PersonalData(ReplacementValue = "***NAME***")]
public string Name { get; set; } = "";

// Logs: {"Name": "***NAME***"}
anchor

Safety ​

  • Depth limit: Recursive [DeepPersonalData] traversal is capped at 10 levels to prevent stack overflows
  • Property getter errors: If a property getter throws, the field is logged as null rather than crashing the pipeline
  • Null values: Null PII properties are logged as null, not the placeholder string

See Also ​