Skip to content

CallEvent ​

CallEvent is a readonly struct passed to the Resilience.OnEvent listener. Raising an event is allocation-free, though the Result member boxes value-type results. Result is populated only when a listener is attached.

MemberDescription
KindThe type of event that occurred.
PolicyNameThe name of the policy (Resilience.Name). This allows a single listener to monitor multiple policies.
AttemptNumberThe 1-based index of the attempt. For Attempt events, it is the attempt that just finished. For Retrying and pre-attempt events, it is the attempt about to run.
VerdictThe classification of the most recent attempt. Defaults to Ok before any attempt has run.
DurationFor Attempt events, this is the duration of that attempt. For all other event kinds, this is the total time the call has been running.
DelayThe duration of the pause about to be served. This is the backoff delay for Retrying events or the rejection pause for the two rejection events. This is null for all other event kinds.
ExceptionThe exception thrown by the most recent attempt, or null if none was thrown.
ResultThe value returned by the most recent attempt, as an object. This is null if the attempt threw an exception or returned nothing.
ReasonThe StopReason indicating why the call stopped. This is populated only for terminal event kinds.
IsRejectiontrue for RejectedByBreaker and RejectedByBudget, for a listener that treats the two refusals alike. false for RejectedByQuota, which refuses one attempt rather than the call.
IsTerminaltrue for the kinds that end a call. Exactly one of these is raised per call.
ToString()Returns a formatted summary of the event, omitting absent segments.
Create(kind, ...)Static. Builds a CallEvent for testing a listener without the executor. Every parameter but kind is defaulted.

CallEventKind ​

The CallEventKind enum defines the event types raised during a call.

KindTerminalDelay PopulatedReason Populated
AttemptNoNoNo
RetryingNoYes (Backoff)No
SucceededYesNoYes (Succeeded)
NotRetriedYesNoYes (Permanent)
ExhaustedYesNoYes (AttemptsExhausted)
RejectedByBreakerYesYes (Rejection pause)Yes (DependencyUnavailable)
RejectedByBudgetYesYes (Rejection pause)Yes (BudgetExhausted)
DeadlineExceededYesNoYes (DeadlineExceeded)
OrphanedWorkNoNoNo
BreakerOpenedNoNoNo
BreakerClosedNoNoNo
BreakerHalfOpenedNoNoNo
NestedRetryNoNoNo
HedgeStartedNoYes (the latency threshold)No
HedgeWonNoNoNo
HedgeDiscardedNoNoNo
HedgeSuppressedNoYes (the latency threshold)No
AttemptCeilingAdaptedNoYes (the measured ceiling)No
BackoffBaseAdaptedNoYes (the measured base)No
StalledNoYes (the stall bound)No
SaturationDetectedNoYes (the queue delay)No
RejectedByQuotaNoYes (the time until the window resets)No
StreamResumedNoNoNo
DrainingYesNoYes (Draining)

Event invariants and behavior ​

  • Terminal events: Every call ends with exactly one terminal event, which is what makes logical operations countable. The IsTerminal property identifies them. Stalled is the one event that can arrive after the terminal event of the call it belongs to, and it is not itself terminal, so the count still holds - see below.
  • Rejections: A refusal names the guard that made it: RejectedByBreaker indicates that the dependency is unavailable; RejectedByBudget indicates that the client is retrying too hard. IsRejection covers both. RejectedByQuota refuses one attempt rather than the call, so it is neither terminal nor an IsRejection - see below.
  • Attempt events: Exactly one Attempt event fires per attempt.
  • Retrying events: Retrying fires before the backoff delay is served, so listeners can report the expected idle time.
  • Orphaned work: OrphanedWork fires when an attempt exceeds its ceiling by more than one second, raised retrospectively the moment the work finally returns.
  • Nested retries: NestedRetry events are raised only by the HTTP handler.
  • Hedging: HedgeStarted carries the live latency quantile that triggered it on Delay. HedgeDiscarded fires when a leg is cancelled because a sibling answered first; its Duration is how long that leg ran. A discarded leg raises no Attempt event, because nothing classified it. HedgeSuppressed fires when a call got slow enough to hedge and the hedge was held back - by SuppressAt or by WinRate - and carries the same threshold on Delay that HedgeStarted does, so the two count against each other. A hedge the retry budget refused, one held back because the call is Sheddable, and one that was never armed at all, raise nothing. See Hedging.
  • Stalls: Stalled fires when a transfer is cut off for lack of progress, and carries the bound that fired on Delay and the AttemptStalledException on Exception. For a response body the caller is reading itself, it arrives after the call has already raised Succeeded - the retry loop was over before the stall existed - which makes it the only event that follows a terminal one. For a stream between two elements, and for a body read under BufferResponses, the stall is inside the attempt and is followed by Retrying or by a terminal event of its own. See progress bounds.
  • Measured backoff bases: BackoffBaseAdapted carries the new base on Delay, after the Spread clamp - which is what the curve actually uses. It is raised on the retry decision, and only when the number differs from the last one raised for that policy instance. A policy whose previous attempt was throttled rather than transient raises nothing, because a throttled retry does not use the measured base. See Retry.
  • Measured attempt ceilings: AttemptCeilingAdapted carries the new ceiling on Delay. It is raised only when the measured term is what bounds the attempt, and only when the number differs from the last one raised for that policy instance - so the rate follows how much the estimate moves rather than how much traffic there is. A policy whose ceiling has been clamped back to AttemptTimeout raises nothing. See Deadlines.
  • Local saturation: SaturationDetected carries the thread-pool queue delay that was measured on Delay. It is raised at the onset of an episode - once when the process crosses Multiple times its own normal queue delay, and nothing more until the queue has drained and filled again - so a count of these is a count of local incidents. The episode is tracked per policy instance, because each policy independently stops feeding its own estimates: a listener attached to five policies sees five events per episode. Nothing is refused and no bound moves, so it is neither a failure nor terminal. See Local saturation.
  • Published quota: RejectedByQuota fires when the allowance the dependency publishes is spent and the HTTP handler refused an attempt without sending it, and carries the time until the published window resets on Delay - which is also the pushback the retry honors. The call still ends with Succeeded, Exhausted or DeadlineExceeded. Duration is zero, because the handler does not hold the call's start. Raised only by the HTTP handler. See the published quota.
  • Breaker transitions: Breaker state transitions are raised on the call that triggered the transition, outside the breaker's internal lock.
  • Checkpointed resume: StreamResumed fires when a stream that failed part-way through was restarted from the caller's last checkpoint, and when an HTTP body that stalled while the caller was reading it resumed with a Range request. AttemptNumber is the restart, counting from one - not the underlying attempt count, which each restart's own retry sequence resets. See Checkpointed resume and resuming a stalled download.
  • Draining: Draining fires when a call stops because the process is shutting down, and carries StopReason.Draining. It replaces the retry the call would otherwise have made, so it is terminal, and the failure reported is the dependency's own rather than a refusal of the library's. Duration is how long the call ran, and Delay is null, because nothing is going to wait. A hedge is not armed while draining either, and raises nothing. See Drain-aware shutdown.

Listener contract ​

The OnEvent listener runs synchronously on the executor's thread.

  • Blocking: A listener that blocks blocks the whole call.
  • Exceptions: Any exception a listener throws is swallowed, so telemetry cannot crash the application.
  • Multiple listeners: policy.WithListener(listener) adds one to whatever is already attached. Assigning OnEvent in a with expression replaces it instead, which drops the telemetry and logging a registration attached.
  • Order: Listeners run in the order they were added.

Log event IDs ​

These are the ILogger records a registered policy writes. An event ID is a contract the moment an alert is built on it, so the numbers below are stable and gated by a test. See Logging in DI for how to filter them.

Every record is written every time unless you opt into sampling, which thins IDs 1000-1005 and 1022-1024 while the policy is healthy and leaves the rest alone.

IDNameDefaultVerboseMessage
1000AttemptSucceededTraceInformation{Policy} attempt {Attempt} succeeded in {ElapsedMs} ms
1001AttemptFailedDebugInformation{Policy} attempt {Attempt} failed in {ElapsedMs} ms: {Verdict} {ErrorType}
1002AttemptLimitedDebugInformation{Policy} attempt {Attempt} was refused by a local limiter before it left the process
1003RetryingDebugInformation{Policy} waiting {DelayMs} ms before attempt {Attempt} after a {Verdict} outcome
1004CallSucceededTraceInformation{Policy} succeeded in {ElapsedMs} ms
1005CallSucceededAfterRetriesDebugInformation{Policy} succeeded on attempt {Attempt} after {ElapsedMs} ms
1006NotRetriedDebugInformation{Policy} stopped after attempt {Attempt}: the outcome was classified Permanent
1007NotRetriedFirstSightingWarningWarning{Policy} did not retry {ErrorType} on attempt {Attempt} because the classifier called it Permanent.
1008ExhaustedDebugInformation{Policy} used all {Attempt} attempts in {ElapsedMs} ms and failed with {ErrorType}
1009DeadlineExceededDebugInformation{Policy} ran out of deadline after {ElapsedMs} ms on attempt {Attempt}
1010RejectedDependencyUnavailableWarningWarning{Policy} refused a call because its circuit breaker is open. Rejections logged quietly since the previous warning: {Suppressed}.
1011RejectedBudgetExhaustedWarningWarning{Policy} refused a retry because the retry budget is exhausted.
1012RejectedRepeatDebugInformation{Policy} refused a call: {Reason}
1013BreakerOpenedWarningWarning{Policy} opened its circuit breaker on attempt {Attempt}.
1014BreakerHalfOpenedInformationInformation{Policy} is probing its dependency: the break duration elapsed and this call is the probe
1015BreakerClosedInformationInformation{Policy} closed its circuit breaker and is taking traffic again
1016OrphanedWorkWarningWarning{Policy} attempt {Attempt} kept running after its timeout, so that work is still going unobserved.
1017OrphanedWorkRepeatDebugInformation{Policy} attempt {Attempt} kept running after its timeout
1018NestedRetryWarningWarning{Policy} is retrying a request that is already inside a retrying client.
1019NestedRetryRepeatTraceInformation{Policy} is retrying inside another retrying client
1020PolicyResolvedDebugInformation{Policy} resolved: {Effective}
1021PolicyClassifierTraceDebug{Policy} classifier: {Rules}
1022HedgeStartedTraceInformation{Policy} started hedge attempt {Attempt}: the call has been running longer than {ThresholdMs} ms
1023HedgeWonTraceInformation{Policy} answered from hedge attempt {Attempt} after {ElapsedMs} ms
1024HedgeDiscardedTraceInformation{Policy} discarded attempt {Attempt} after {ElapsedMs} ms because a sibling answered first
1025AttemptCeilingAdaptedDebugInformation{Policy} measured a new per-attempt ceiling of {CeilingMs} ms from recent latency
1026BackoffBaseAdaptedDebugInformation{Policy} measured a new backoff base of {BaseMs} ms from recent latency
1027HedgeSuppressedDebugInformation{Policy} held back hedge attempt {Attempt} after {ThresholdMs} ms
1028StalledWarningWarning{Policy} cut off a transfer after {TransferredCount} byte(s) or element(s): nothing arrived for {StallMs} ms
1029SaturationDetectedDebugInformation{Policy} stopped measuring: this process's thread pool is queueing for {QueueDelayMs} ms
1030RejectedByQuotaWarningWarning{Policy} refused an attempt because the allowance the dependency publishes is spent, and it resets in {ResetMs} ms.
1031StreamResumedDebugInformation{Policy} resumed a stream on attempt {Attempt} from the caller's last checkpoint
1032DrainingDebugInformation{Policy} stopped after attempt {Attempt} in {ElapsedMs} ms without retrying: this process is draining, and failed with {ErrorType}

Event 1032 is written once per drained call, so its rate during a rollout is a count of calls the shutdown cut short.

Field names are shared with the metric tag vocabulary wherever both exist (Policy, Verdict, Reason), so a structured record and a metric describe the same call with the same words.

Events 1010, 1011, 1016, 1018 and 1030 are rate-limited per policy - see flood control. Events 1007, 1012, 1017 and 1019 are the quiet forms the suppressed occurrences take.

Released under the MIT License.