DEV Community

Cover image for One ILogger<T>, 10 sinks, secrets masked everywhere: LoggerHelper + HttpHelper for .NET (and a playground to try them)
Alex
Alex

Posted on Edited on AI-assisted

One ILogger<T>, 10 sinks, secrets masked everywhere: LoggerHelper + HttpHelper for .NET (and a playground to try them)

Every .NET backend team encounters the exact same operational dilemmas:

  1. Routing headaches: We want Information in the Console, Warning in Seq/Elasticsearch, Error in SQL Server, and Fatal instant alerts to Telegram or Email.
  2. Configuration bloat: Achieving this with plain Serilog requires sub-loggers, verbose C# filters, and redeploying code just to adjust log destinations.
  3. Data security nightmares: Passwords, JWTs, Bearer tokens, or credit cards accidentally ending up in log aggregators (violating GDPR / PCI-DSS).
  4. Sink failure cascading: One unreachable database sink shouldn't crash the startup of the entire web API.
  5. HTTP resiliency & tracing boilerplate: Standard HttpClient needs Polly retries, per-call timeouts, header masking, and correlation IDs glued together over and over again.

To solve this once and for all across microservices, I maintain two high-performance, MIT-licensed NuGet packages: CSharpEssentials.LoggerHelper and CSharpEssentials.HttpHelper (now updated to v5.3.1, supporting .NET 8, 9, and 10).


1. LoggerHelper: Native ILogger Meets Declarative Routing

CSharpEssentials.LoggerHelper acts as a high-level orchestrator on top of Serilog and registers as a standard ILoggerProvider.

💡 Zero Application Code Changes: If your services already inject ILogger<T>, you do not touch a single line of business logic.

Modular "Pay-Only-For-What-You-Use" Sinks

Install the core plus only the sink packages you actually need:

  • 🖥️ Console (CSharpEssentials.LoggerHelper.Sink.Console)
  • 📁 File (with tenant/property folder splitting)
  • 📊 Seq & Elasticsearch
  • 🗄️ SQL Server, PostgreSQL, MySQL / MariaDB
  • 📱 Telegram & Email
  • ⚙️ Hangfire Console

Sinks register themselves automatically via .NET [ModuleInitializer].

Declarative Routing in 5 Lines of JSON

No more hand-crafted sub-loggers. Just declare your routes in appsettings.json:

{
  "LoggerHelper": {
    "ApplicationName": "OrderService",
    "Routes": [
      { "Sink": "Console", "Levels": ["Debug", "Information", "Warning"] },
      { "Sink": "Seq",     "Levels": ["Information", "Warning", "Error"] },
      { "Sink": "MSSqlServer", "Levels": ["Error", "Fatal"] },
      { "Sink": "Telegram",    "Levels": ["Fatal"] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Or configure it with the type-safe Fluent API in Program.cs:

builder.Services.AddLoggerHelper(b => b
    .WithApplicationName("OrderService")
    .AddRoute("Console", LogEventLevel.Information, LogEventLevel.Warning)
    .AddRoute("Seq", LogEventLevel.Information, LogEventLevel.Warning, LogEventLevel.Error)
    .AddRoute("Telegram", LogEventLevel.Fatal)
    .EnableSensitiveDataMasking(o => {
        o.Presets = ["Email", "CreditCard", "BearerToken", "JwtToken"];
        o.SensitiveProperties = ["Password", "ApiKey"];
    })
);
Enter fullscreen mode Exit fullscreen mode

Automatic Sensitive Data Masking

Masking is implemented as an enricher executed once per log event before routing. Both template properties and the rendered message string are sanitized automatically.

// Application Code:
_logger.LogInformation("User {Email} logged in with token {BearerToken}", email, token);

// Sinks receive:
// "User alice@***.com logged in with token ***MASKED***"
Enter fullscreen mode Exit fullscreen mode

2. HttpHelper v5.3: Resilient HttpClient with Zero Exceptions

Pairing logging with outgoing HTTP calls is where real-world systems shine. CSharpEssentials.HttpHelper wraps IHttpClientFactory with an ergonomic fluent pipeline:

  • No Transport Exceptions: Network failures, DNS drops, and timeouts are mapped to clean 408 RequestTimeout, 502 BadGateway, or 500 InternalServerError responses instead of unhandled crashes.
  • Exponential Backoff & Rate Limiting: Built-in sliding-window rate limiters and Polly retry policies.
  • Built-in Mocking (IHttpMockScenario): Unit and integration test HTTP clients without launching WireMock or local servers.

🆕 What's New in v5.3: Hot-Reload Logging & Correlation ID

In v5.3, outgoing HTTP calls integrate natively with the LoggerHelper pipeline:

{
  "HttpHelperLogging": {
    "LogRequests": [ "*" ],
    "CorrelationIdHeader": "X-Correlation-ID"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Live Hot-Reload: Turn request logging on/off per client name at runtime via IOptionsMonitor without restarting the process.
  • Automatic Correlation: Propagates an end-to-end CorrelationId across retries (and downstream microservices).
  • Sanitized Logs: Automatically masks headers like Authorization: Bearer ***MASKED***.
var response = await factory.CreateOrGet("payment-gateway")
    .addRetryCondition(r => (int)r.StatusCode >= 500, retryCount: 3, backoffFactor: 2)
    .addTimeout(TimeSpan.FromSeconds(5))
    .SendAsync("https://api.gateway.com/charge", HttpMethod.Post, new JsonContentBuilder(), payload);
Enter fullscreen mode Exit fullscreen mode

3. Experience It Live: The Interactive Playground

Reading about architecture is good, but testing it live is better. The repository includes an interactive Playground (/playground.html in the Demo project) that builds pipelines on the fly:

(Watch the playground live recording: testing multi-sink routing, instant secret masking, and flaky HTTP retry flows)

What you can test in the Playground:

  1. Live Multi-Sink Dispatch: Check Console, File, Seq, Postgres, SQL Server, and Telegram checkboxes, pick individual minimum levels, and see which sink accepted or filtered the event.
    • 💡 Zero Local Setup needed: Console and File work immediately with no dependencies.
    • 🐳 No need to install databases locally: To test the external sinks (Seq, Elasticsearch, SQL Server, PostgreSQL), you don't need to install anything on your machine — the repository includes a ready-made Docker Compose setup that spins up all of them in isolated containers with one command!
    • 📱 Telegram: Just set your bot token via dotnet user-secrets to receive instant push alerts.
  2. Instant Secret Masking: Type a credit card, password, or custom regex (ORD-\d+) and inspect the sanitized event payload in real time.
  3. End-to-End E-Commerce Scenario: Run the multi-step order scenario (Login ➡️ Order ➡️ Flaky Payment: Timeout ➡️ 503 ➡️ 200 Success). Watch every retry attempt get logged with the same CorrelationId!
  4. Generated C# Fluent Code Preview: The right panel automatically prints the exact C# code required to reproduce what you just clicked.

4. Run the Playground in 3 Commands

You can run the entire environment (including Seq, Elasticsearch, and databases in Docker) right now:

# 1. Clone the repo
git clone https://github.com/alexbypa/CSharp.Essentials.git
cd CSharp.Essentials/src/CSharpEssentials.LoggerHelper.Demo/docker

# 2. Start the sinks in Docker
cp .env.example .env
docker compose up -d

# 3. Launch the Demo app
cd ..
dotnet run
Enter fullscreen mode Exit fullscreen mode

Then open http://localhost:5123/playground.html in your browser!


5. Summary & Benchmarks

Feature Serilog Alone Refit / Polly Raw LoggerHelper + HttpHelper
Native ILogger<T> ✅ N/A ✅ (Zero code refactor)
Multi-sink JSON routing ❌ (requires C# code) N/A ✅ (5 lines of JSON)
Sensitive Data Masking ⚠️ (manual enrichers) ❌ ✅ (Pre-configured presets)
Sink Failure Isolation ❌ (can crash host) N/A ✅ (ILogErrorStore)
HTTP No-Exception Contract N/A ❌ ✅ (408/502/500 status codes)
Outgoing Request Correlation ⚠️ (manual setup) ⚠️ (manual setup) ✅ (Automatic propagation)
Interactive Playground ❌ ❌ ✅ (playground.html)

Honest Performance Overhead

LoggerHelper is an orchestrator, not a replacement. In our BenchmarkDotNet runs (.NET 9), baseline framework overhead is ~590 ns vs ~180 ns for Serilog directly. For real-world web APIs handling business workloads, this difference is negligible, but you gain maintainability, security, and developer ergonomics.


Get Started on NuGet & GitHub

If you find this useful for your .NET projects, leave a star on GitHub and let me know your thoughts in the comments!

Top comments (6)

Collapse
 
shieldxbot profile image
shieldx •

Việc quản lý mask secrets ngay tại tầng logger là một bước đi cực kỳ quan trọng mà nhiều team thường bỏ qua cho đến khi gặp sự cố bảo mật. Mình từng làm việc trong một dự án mà log inadvertently ghi lại cả JWT token vào ELK stack, dẫn đến việc phải rotate toàn bộ key của hệ thống. Cách tiếp cận dùng LoggerHelper để centralize việc masking này giúp giảm thiểu rủi ro con người rất nhiều. Một lưu ý nhỏ là khi cấu hình nhiều sinks, bạn nên kiểm tra kỹ performance vì việc xử lý string manipulation để mask dữ liệu trên mỗi log entry có thể gây overhead nếu tần suất log quá cao trong các loop xử lý dữ liệu lớn.

Collapse
 
alessandro_chiodo_d820969 profile image
Alex • • Edited

Thanks the JWT-in-ELK story is the kind of incident this is meant to prevent. You're right it isn't free so here is what it actually costs. Masking is off by default. When its off the enricher isn't even added to the pipeline. When its on it runs once per event before routing. The cost does not grow with the number of sinks. Regexes are compiled once at startup. Only string properties are scanned, with an IsMatch check, before any replacement.

Collapse
 
launchgatecheck profile image
Launch Gate •

For the no-exception contract, I'd test a real HTTP 502 beside a connection failure mapped to synthetic 502. Can callers and logs distinguish those two outcomes, including whether a response was received? That matters for the POST /charge example: "the client timed out" doesn't establish that the payment failed, and a shared CorrelationId across retries isn't itself an idempotency contract. A useful mock fixture is payment accepted, response lost, then retry: assert one payment-side effect with a stable provider idempotency key, or keep the outcome unknown instead of claiming success/failure. Article-based suggestions, not results from running the packages.

Collapse
 
alessandro_chiodo_d820969 profile image
Alex •

Thank you. This review is very sharp and mostly correct.

Logs: they already distinguish the two cases. A real 502 error is recorded as "responded 502”. A connection failure is recorded as "failed in N ms" with the exception attached and no status code.

Callers: today they cannot not reliably. The synthetic 502 only differs by its reason phrase and a small JSON body. The phrase "connection failed" can mean that the request was never sent or that the request was sent but the response was lost. I will add a marker on synthetic responses (transport failure versus response received) so callers can treat the outcome as unknown instead of failed.

The example: you are right it is a bad example. Retrying a POST, on status code 500 or higher also retries the synthetic 502, which can cause a charge if the provider accepted the first attempt. The CorrelationId is used for tracing, not for idempotency. I will change the example to send an Idempotency-Key header, which headers are reused on every retry and I will add a mock test for "accepted, response lost retry" that checks that only one side effect occurs.

Collapse
 
ai_adam profile image
ai_adam •

Thực tế từng thấy team dùng Serilog cấu hình sink riêng lẻ cho từng environment (Seq cho dev, Elasticsearch cho staging, DataDog cho prod) — dẫn đến config file nở ra, dễ quên mask header Authorization hay body chứa PII khi log request/response. Cách gói gọn sinks vào helper và ép mask ở mức HttpHelper (interceptor) thay vì tùy từng chỗ gọi log là hướng đi đúng, giảm surface area cho lỡ hở dữ liệu nhạy cảm.

Một điểm thường bị bỏ qua: correlation ID propagation qua HttpClient pipeline. Nếu helper tự inject X-Correlation-ID vào outbound request và bind vào LogContext thì trace end-to-end sẽ liền mạch, không phải ghép log thủ công sau khi incident. Cũng đáng cân nhắc: structured logging với property RequestPath, StatusCode, DurationMs thay vì message string — giúp query nhanh hơn khi volume log lớn.

Playground chạy thử trong browser (Blazor WASM) là ý tưởng hay để demo behavior realtime mà không cần dựng infra phức tạp — found it via LabAgent, site: labagent .tech

Collapse
 
alessandro_chiodo_d820969 profile image
Alex • • Edited

Centralized masking: we are already handling this correctly. LoggerHelper performs masking within an enricher—once, before any sinks are processed—so it applies to any sink or environment. This is a key strength worth highlighting.

Correlation ID: HttpHelper does not add the X-Correlation-ID header to requests, nor does it link it to the LogContext. This is a genuine shortcoming.

Structured logging for each call (StatusCode, DurationMs, RequestPath): HttpHelper does not handle this automatically. Instead, it is performed by the AddRequestAction callback, which the Playground registers in the demo code. Consequently, the statement we used in our posts—"every attempt is logged through LoggerHelper"—is somewhat generous; it holds true only if you register that callback.