Skip to content

Telemetry

When a call is slow or fails in production, you need visibility into the policy's behavior - such as which attempts are being retried, how long they take, whether a circuit breaker opened, or if the retry budget is exhausted. Telemetry provides this visibility through a single event stream.

Telemetry is enabled by default for policies registered in a container. For policies built manually, it is opt-in. If OnEvent is null, the executor raises no events and incurs no performance overhead.

The telemetry system uses a single struct, CallEvent, and a single delegate, Resilience.OnEvent.

Attach a listener

You can attach a listener to a policy to log or record events.

csharp
var api = Resilience.Http with
{
    Name = "payments",
    Backoff = Backoff.None,
    OnEvent = e => _logger.LogInformation(
        message: "{Policy} {Kind} attempt {Attempt}: {Verdict} in {Ms}ms",
        e.PolicyName, e.Kind, e.AttemptNumber, e.Verdict.Kind, e.Duration.TotalMilliseconds),
};

The listener is synchronous and runs on the same thread as the executor. To avoid blocking the call, only perform fast operations such as logging, counting, or enqueuing; do not perform synchronous I/O. Any exception thrown by a listener is swallowed to prevent telemetry from failing the operation it is observing.

To use multiple listeners, combine them using the + operator: OnEvent = first + second.

A lambda is still the right answer for anything that is not an ILogger. If it is, a ready-made listener already exists and says what each event means: see Logging.

Event types

KindDescriptionTerminal?
AttemptAn attempt finished, regardless of the verdictNo
RetryingA retry was decided and the backoff delay is about to startNo
SucceededThe call succeededYes
NotRetriedThe outcome was PermanentYes
ExhaustedThe final attempt failed and no retries remainYes
RejectedA circuit breaker or the retry budget refused the callYes
DeadlineExceededThe total wall-clock budget expiredYes
OrphanedWorkA callback ran past the timeout that should have stopped itNo
BreakerOpened / BreakerClosed / BreakerHalfOpenedA circuit breaker changed stateNo
NestedRetryThe request is already inside another retrying clientNo

Every call ends with exactly one terminal event. This invariant ensures that counts of logical operations are accurate.

csharp
var api = Resilience.Default with { Backoff = Backoff.None, OnEvent = events.Record };

await api.RunAsync(attempt => calls.NextAsync(cancellationToken: attempt), cancellationToken: cancellationToken);

// Attempt, Retrying, Attempt, Succeeded
Console.WriteLine(value: string.Join(separator: ", ", values: events.Kinds));
  • Duration represents the individual attempt's duration for Attempt events, and the total elapsed time for all other event types.
  • Delay represents the pause about to be served for Retrying and Rejected events; it is null for other events.
  • Reason distinguishes between the two types of refusals covered by a Rejected event.
csharp
// [PolicyName] Kind #N VerdictKind ExceptionType (duration) +delay
Console.WriteLine(value: events[index: 0]); // [api] Attempt #1 Ok (0.1ms)

For more details, see the CallEvent reference.

Metrics and traces

The NResilience.Extensions package provides a meter, an activity source, and a listener that feeds both.

csharp
// A policy registered in a container is instrumented for you. A policy in a static field
// is not - this says it.
var api = (Resilience.Http with { Name = "payments" }).WithTelemetry();
InstrumentUnitDescription
nresilience.calls{call}Total logical operations
nresilience.attempts{attempt}Total wire-level attempts
nresilience.rejections{rejection}Calls refused by a guard, tagged dependency_unavailable or budget_exhausted
nresilience.call.durationsEnd-to-end duration of a logical operation
nresilience.attempt.durationsDuration of a single attempt
nresilience.limiter.leases{lease}Permits a limiter was asked for, tagged acquired or denied
nresilience.limiter.wait.durationsHow long a caller waited on a limiter. Zero unless queueing is enabled

The retry fraction is calculated as nresilience.attempts ÷ nresilience.calls. This is the primary metric for monitoring retry feedback loops and identifying potential retry storms.

For more information, see Telemetry in DI.

Released under the MIT License.