Skip to content

Observability

One event stream

Every policy reports what it does as a typed Event: retries, breaker transitions, rejections, throttle adjustments, stale cache serves.

from nopanic import events

cancel = events.subscribe(
    lambda e: metrics.increment(e.kind, tags={"name": e.name, "function": e.function})
)
Field Meaning
kind e.g. retry.attempt_failed, breaker.state_change
policy the emitting policy type
name the policy's name= (every policy takes one), or None
function qualified name of the guarded function, or None for blocks
data event-specific payload

Listeners run synchronously on the calling thread; keep them fast. A listener that raises is logged and ignored: observability never breaks the call it observes. With no listeners, emitting costs a single attribute read.

Event kinds

Kind Data
retry.attempt_failed attempt, delay, exception (and result for on_result retries)
retry.gave_up attempt, exception, reason (attempts, giveup, backoff, max_time, deadline, budget)
breaker.state_change old, new
breaker.rejected retry_after
bulkhead.rejected max_concurrent, max_wait
ratelimit.waited seconds
ratelimit.rejected would_wait, max_wait
ratelimit.throttled old_rate, new_rate, retry_after
timeout.expired seconds, function
timeout.saturated abandoned, max_abandoned
fallback.used exception
hedge.launched delay
cache.stale_served age, exception
breaker.store_error, ratelimit.store_error exception, fail_open (nopanic-redis)

Logging in one line

import logging
from nopanic import events

events.log_to(logging.getLogger("resilience"))   # level=logging.INFO by default

One line per event, with extra fields nopanic_kind, nopanic_policy, nopanic_name and nopanic_function for structured-logging handlers.

Live state

For dashboards and health checks, read state directly:

Policy Property
circuit_breaker .state, .metrics
bulkhead .in_flight
rate_limit, adaptive_rate_limit .available_tokens, .current_rate (adaptive)
timeout .abandoned
cache .size
RetryBudget .available
keyed .keys(), .get(key)
@app.get("/health")
def health():
    m = payments.metrics
    return {"payments": m.state, "failure_rate": m.failure_rate}

OpenTelemetry

pip install nopanic-otel
import nopanic_otel

nopanic_otel.instrument()                        # counters, histograms, span events
nopanic_otel.observe(payments, reports, quota)   # observable gauges

instrument() records every event as the nopanic.events counter (attributes nopanic.kind, nopanic.policy, nopanic.name, code.function), retry and rate-limit waits as the nopanic.retry.delay and nopanic.ratelimit.wait histograms, and adds an event to the current span, so each trace shows the retries, rejections and transitions that happened inside that request. observe() registers gauges for breaker state and rates, bulkhead in-flight calls, rate-limit tokens and adaptive rate, abandoned timeout workers, cache size and retry budget; keyed policies are expanded with a nopanic.key attribute. Both return a callable that undoes them. Only opentelemetry-api is required; configure the SDK as usual.