Every .NET team I've worked with ends up with the same logging wish list:

  • errors to a database, everything to a file, critical stuff to Telegram or email;
  • a different minimum level for each destination;
  • passwords, tokens and card numbers never leaving the process in clear text;
  • one broken sink must not take the app down;
  • HTTP calls that retry, time out and log themselves without boilerplate.

With plain Serilog and HttpClient all of this is possible, but it is a lot of JSON, sub-loggers, filters and Polly glue, rewritten in every service. I maintain two MIT packages that do it for you.

LoggerHelper: routing, masking, resilience

CSharpEssentials.LoggerHelper sits on top of Serilog and registers as a standard ILoggerProvider, so code that already uses ILogger<T> doesn't change. You install the core plus only the sinks you need: Console, File, Email, Telegram, SQL Server, PostgreSQL, MySQL/MariaDB, Elasticsearch, Seq, Hangfire Console.

Routing is declarative, in JSON or with the fluent API:

"Routes": [ { "Sink": "Console", "Levels": ["Debug", "Information", "Warning"] }, { "Sink": "File", "Levels": ["Information", "Warning", "Error", "Fatal"] }, { "Sink": "Telegram", "Levels": ["Error", "Fatal"] }, { "Sink": "Email", "Levels": ["Fatal"] } ] 

What you get on top of plain Serilog:

  • Sensitive data masking in every sink: presets (passwords, JWTs, cards, emails), property names and your own regex, rendered message included.
  • Failure isolation: a sink with a missing or wrong configuration is reported as failed, the others keep working.
  • A built-in dashboard (/loggerhelper) with loaded sinks, errors and the logs that came right before an error.
  • An optional MCP server, so an AI assistant can check sink health and change levels at runtime.

HttpHelper: HttpClient without the boilerplate

CSharpEssentials.HttpHelper wraps IHttpClientFactory: named clients from appsettings.json, Polly retries, per-call timeouts, rate limiting, per-request callbacks and built-in mocks for tests. Transport failures come back as 408/502/500 responses instead of exceptions, and every attempt is logged through LoggerHelper with the Authorization header masked.

See both of them working: the Playground

Reading about routing only gets you so far, so the repo has a page where you can click it.

LoggerHelper Playground

Two commands to start

git clone https://github.com/alexbypa/CSharp.Essentials cd CSharp.Essentials/src/CSharpEssentials.LoggerHelper.Demo/docker cp .env.example .env docker compose up -d # SQL Server, PostgreSQL, Seq, Elasticsearch cd .. && dotnet run # open http://localhost:5123 

No Docker? Tick only Console and File: they need nothing else.

Three experiments worth two minutes

1. Masking that also covers the rendered message

Add a property Password = Sup3rSecret!, enable masking and press Send log. Every sink (Seq, Elasticsearch, both databases, the file) receives ***MASKED***, in the property and in the rendered message. That second part matters: if you render the message before masking, the secret leaks through RenderedMessage. Building this page is how I found that bug in my own library, and 5.2.7 fixes it.

2. One minimum level per sink

Set PostgreSQL to Error and send a Warning. The result panel shows PostgreSQL as filtered and the other sinks as received. That's the whole routing idea: each sink gets only the levels you ask for.

3. A broken sink does not break the app

docker compose stop seq, then send again. The request still succeeds, the Seq error shows up in the result panel and the other sinks still write. In production that's the difference between "we lost Seq for an hour" and "we lost the app for an hour".

HttpHelper in the Playground

The page also drives HttpHelper against httpbin.org: a flaky upstream that retries 500 → 502 → 500 → 200, a timeout returned as 408, a rate limit returned as 429 without touching the network, a Bearer token that every sink logs as Bearer ***MASKED***. Each attempt is a log event in the sinks you picked.

HttpHelper in the Playground

What the page actually runs

Every click builds a fresh pipeline with the public fluent API only, writes the event and disposes the logger to flush the batching sinks. Simplified:

services.AddLoggerHelper(b => { b.WithApplicationName("LoggerHelperPlayground") .AddRoute("Seq", LogEventLevel.Information, LogEventLevel.Warning, LogEventLevel.Error) .AddRoute("PostgreSql", LogEventLevel.Error, LogEventLevel.Fatal) .ConfigureSeq(s => s.ServerUrl = "http://127.0.0.1:5341") .ConfigurePostgreSql(p => { p.ConnectionString = connectionString; p.TableName = "playground_logs"; }) .EnableSensitiveDataMasking(o => o.SensitiveProperties = ["Password"]); }); 

The same configuration also works from appsettings.json if you prefer JSON.

The honest part

  • The Playground covers 7 of the 10 sinks today: Console, File, Seq, Elasticsearch, SQL Server, PostgreSQL and Telegram. Email, MySQL/MariaDB and Hangfire Console are not in the Playground yet. They ship as packages and are documented in their READMEs, you just can't click them on the page.
  • LoggerHelper orchestrates Serilog, so it adds per-message overhead compared with a hand-tuned Serilog setup. The numbers are in the benchmarks doc. If you log millions of events per second, measure first.

Try it, break it, tell me

  • Walkthrough with seven experiments: DEMO.md
  • Docs: loggerhelper.it
  • NuGet: dotnet add package CSharpEssentials.LoggerHelper

Which sink should the Playground get next? Feedback on the fluent API is very welcome in the comments.