4 min readRunning it for real

Logs that don't flood

How NResilience handles logging: silent when healthy, informative when failing, and flood-controlled to prevent log-noise during incidents.

loggingobservability

TL;DR: Logging should be an alarm, not a background hum. NResilience uses a profile-based system that stays silent during healthy operations and employs a repeat-window to prevent a single failing call from flooding your logs with identical retry records.

The "Log Noise" Problem

In most systems, logging is binary: it's either on or off. When you turn on Debug or Information logging for a resilience library, you often get a wall of text: "Attempt 1 started", "Attempt 1 failed", "Retrying in 100ms", "Attempt 2 started".

In a high-throughput system, this is catastrophic. A single dependency brownout can generate millions of nearly identical log lines in seconds, filling up your disks and making it impossible to find the actual root cause in your log aggregator.

NResilience takes the opposite approach: the library decides the level, and the operator decides the filter.

Profiles: Silent by Default

Instead of relying on generic log levels, NResilience uses profiles. This allows the library to encode the "meaning" of an event into its severity.

You can choose from three profiles:

  • Off: Attaches no listener, so suppressed calls cost nothing.
  • Default: Healthy traffic is Trace, retried-then-successful calls are Debug, incidents are Warning.
  • Verbose: Raises every traffic-proportional record to Information and leaves incident records where they are. Exists for sinks that enforce a minimum level.

The key is that in the Default profile, a healthy process writes nothing above Trace. A Warning means a breaker opened, a budget was exhausted, a callback outlived its timeout, or a nested retry was detected - the library encodes the meaning of an event into its severity, so a healthy call (even one that retried) stays out of your Information feed.

Logging is on by default for policies registered in a container, and opt-in for policies you build yourself:

// A policy registered in a container logs for you. A policy in a static field does not.
var payments = (Resilience.Http with { Name = "payments" }).WithLogging(logger: logger);

Flood Control: The Repeat Window

Even when a call is failing, you don't need to see every single retry. If a call is retrying 10 times, and every attempt fails with the same 503 Service Unavailable, 10 log lines are not 10x more useful than one.

NResilience implements flood control via a repeat-window (30 seconds by default, per policy and reason). When a guard refuses a call, the first refusal writes a Warning. Refusals within the window are counted and written as Debug records, and the count is included in the next warning: "Rejections logged quietly since the previous warning: 1483."

No records are dropped, only demoted. You still see every rejection at Debug if your filter carries it - the window keeps the Warning stream legible during a total dependency collapse.

Per-Policy Filtering

The NResilience logging system handles categories well. Every policy logs under its own unique category: NResilience.<PolicyName>.

This means you can keep the entire library at Warning level, but turn on Debug logging for just one specific, problematic dependency in your appsettings.json without a redeploy:

{
  "Logging": {
    "LogLevel": {
      "NResilience": "Warning",
      "NResilience.payments": "Debug",
      "NResilience.identity": "Information"
    }
  }
}

This allows you to "zoom in" on a specific part of your distributed system while keeping the rest of the noise filtered out.

Sampling for Scale

For ultra-high-throughput services, even "silent-when-healthy" logging can be too much during an incident. NResilience supports opt-in log sampling.

LogSampling.OneIn(n) keeps one traffic record in n while the policy is healthy, and every record for a minute after an incident - so you get a consistent stream of data when it matters, and a sampled rate the rest of the time. The counting is exact rather than random: every nth record, per record and per policy.

services.AddResilienceLogging(o => o.Sampling = LogSampling.OneIn(keepOneIn: 20));

Integration with WithLogging()

If you are using the NResilience.Extensions package, logging is often handled for you. When you register policies in a DI container, the library automatically attaches a log listener.

If you need to tune the profile or levels, use WithLogging() with a ResilienceLoggingOptions callback - it takes a logger first, and a level-override function or profile in the options.

An explicit WithLogging call wins over the automatic container-based listener, so your specific tuning takes precedence. At most one log listener attaches per policy, and the first one attached wins.

Conclusion

Logs are only useful if they are actionable. By shifting the responsibility of "level" to the library and providing tools like repeat-windows and per-policy categories, NResilience ensures that your logs are a signal, not noise. You get the information you need when things break, and total silence when they don't.

The Logging documentation covers configuring profiles and sampling. For what your logs look like when requests start crossing service boundaries, Deadlines that travel covers deadline propagation and nested retry detection.