DEV Community

Hello Cerbi
Hello Cerbi

Posted on

Adding Log Governance to Serilog in .NET — Without Rewriting a Single Call Site

Adding Log Governance to Serilog in .NET — Without Rewriting a Single Call Site

If you've shipped a .NET application in the last few years, there's a good chance Serilog is already in your stack. It's battle-tested, flexible, and the sink ecosystem is massive. But here's the thing nobody talks about: Serilog doesn't care what you log. It will happily ship a patient's SSN, a raw JWT, or a credit card number to Elasticsearch without blinking.

That's not a Serilog problem — it's a governance problem. And it's one I ran into repeatedly while working on distributed .NET systems before I built Cerbi.

The Gap Between "Structured Logging" and "Safe Logging"

Structured logging is great. You get queryable fields, consistent shapes, and tooling that actually works. But structure doesn't mean compliance. A perfectly structured log event can still contain PII that violates HIPAA or GDPR. The structure just makes it easier to find during an audit — which cuts both ways.

The industry response has been to bolt on post-collection filtering: Datadog has sensitive data scrubbing, Splunk has masking rules, Elastic has field-level security. These all work after the data has already left your application. If your pipeline has any delay, misconfiguration, or side-channel (a dev environment, a debug sink, a shared log file) — the data escaped.

Governance before emission is the only way to guarantee containment. That's the design principle behind CerbiStream.

CerbiStream + Serilog: How It Actually Works

CerbiStream ships a dedicated Serilog package: Cerbi.Serilog.Governance. It hooks into Serilog's enricher/sink pipeline and enforces your governance profile before any sink receives the event.

Install both packages:

dotnet add package CerbiStream
dotnet add package Cerbi.Serilog.Governance
Enter fullscreen mode Exit fullscreen mode

Then wire it up in your Program.cs:

using CerbiStream.Governance;
using Cerbi.Serilog.Governance;

var governanceConfig = new GovernanceProfileBuilder()
    .EnforceRequiredFields("CorrelationId", "ServiceName", "Environment")
    .RedactFields("SSN", "CreditCardNumber", "PatientId", "AuthToken")
    .BlockIfMissing("CorrelationId")
    .Build();

Log.Logger = new LoggerConfiguration()
    .Enrich.WithCerbiGovernance(governanceConfig)   // <-- governance hook
    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {Message:lj} {Properties}{NewLine}{Exception}")
    .WriteTo.Seq("http://localhost:5341")
    .CreateLogger();

builder.Host.UseSerilog();
Enter fullscreen mode Exit fullscreen mode

That's the entire integration. No changes to existing Log.Information(...) calls anywhere in your codebase. The enricher intercepts every log event, runs the governance checks, redacts what needs redacting, and either passes the event downstream or blocks it — all in under a millisecond.

What "Governance Before Emission" Looks Like in Practice

Here's a realistic scenario. You have a service that logs user activity, and somewhere deep in a helper method, someone wrote this six months ago:

_logger.LogInformation("Processing request for user {UserId} with token {AuthToken}", userId, token);
Enter fullscreen mode Exit fullscreen mode

Without governance, that AuthToken value goes straight into your log aggregator. With Cerbi.Serilog.Governance and RedactFields("AuthToken") configured, the emitted event looks like this:

{
  "UserId": "usr_8821",
  "AuthToken": "[REDACTED]",
  "CorrelationId": "f3a1c7b2-...",
  "ServiceName": "UserActivityService",
  "Environment": "production"
}
Enter fullscreen mode Exit fullscreen mode

The field is still present (so your log schema doesn't break), but the value is gone before it hits any sink. No Elasticsearch index, no Seq stream, no debug console — nothing receives the raw token.

Schema Enforcement Alongside Redaction

The other half of the governance config is enforcement of required fields. This matters in microservices where teams are moving fast and log schemas drift over time. You add BlockIfMissing("CorrelationId") and suddenly every service that forgets to propagate the correlation ID will emit a governance violation instead of a silent gap in your trace.

You can configure this to either block the event entirely, emit a warning event with governance metadata, or — in development — throw an exception so the problem gets caught before it ever reaches staging. Configure the behavior per environment:

var governanceConfig = new GovernanceProfileBuilder()
    .EnforceRequiredFields("CorrelationId", "ServiceName")
    .RedactFields("SSN", "AuthToken", "CreditCardNumber")
    .OnViolation(isDevelopment 
        ? ViolationBehavior.ThrowException 
        : ViolationBehavior.EmitWarningAndProceed)
    .Build();
Enter fullscreen mode Exit fullscreen mode

Using a Signature Pack Instead of Manual Config

If you're targeting a specific compliance standard (HIPAA, GDPR, PCI DSS, SOC 2, etc.), you don't have to enumerate fields manually. Cerbi ships pre-built Signature Packs that encode the relevant field patterns and rules:

dotnet add package Cerbi.Signatures.Hipaa
dotnet add package Cerbi.Signatures.Gdpr
Enter fullscreen mode Exit fullscreen mode
var governanceConfig = new GovernanceProfileBuilder()
    .ApplySignaturePack(new HipaaSignaturePack())
    .ApplySignaturePack(new GdprSignaturePack())
    .Build();
Enter fullscreen mode Exit fullscreen mode

The packs cover the field patterns, redaction rules, and required audit fields for each standard. You can stack multiple packs and add your own custom rules on top.

Overhead

CerbiStream adds less than 1ms per log event. I've run this under heavy load in ASP.NET Core services doing 10k+ requests/minute and the governance layer is invisible in profiling output. The enricher path is synchronous and allocation-minimal — no async overhead, no reflection per-event.

Try It

CerbiStream and all the governance packages are MIT-licensed and free forever. If you want centralized policy management, schema dashboards, and drift alerting across teams, that's CerbiShield — but the SDK works standalone without it.

  • 📦 NuGet: dotnet add package CerbiStream / dotnet add package Cerbi.Serilog.Governance
  • 🔗 GitHub: github.com/Zeroshi/CerbiStream
  • 🌐 Docs & CerbiShield trial (14 days, no credit card): cerbi.io

If you're already on Serilog, this is a one-afternoon integration that closes a real compliance gap. Happy to answer questions in the comments.

— Thomas Nelson

Top comments (0)