Integrations
Emit state transitions over nestjs-diagnostics, mirror them onto @nestjs/event-emitter, and make breaker keys tenant-aware through nestjs-context — all soft-detected and optional.
Resilience plugs into the rest of the ecosystem through event sinks and a context accessor. Every integration is optional and soft-detected — nothing is a hard dependency.
Events
Every state transition is a ResilienceEvent handed to an EventSink. Five transitions are emitted today — one per breaker/failover state change:
type ResilienceEventType =
| 'circuit-opened' // breaker tripped: failures crossed the threshold
| 'circuit-closed' // a half-open probe succeeded; traffic restored
| 'circuit-half-open' // cooldown elapsed; this call is the trial probe
| 'short-circuited' // call rejected while the breaker is open
| 'failover'; // a failover policy moved to the next target
interface ResilienceEvent {
type: ResilienceEventType;
key?: string; // the breaker key, when applicable
[extra: string]: unknown; // e.g. failover carries target / index / error
}
type EventSink = (event: ResilienceEvent) => void;Those five are the whole event surface. The timeout and retry policies don't emit events — they're not in ResilienceEventType, and there's no aviary:resilience:timeout or aviary:resilience:retry channel to subscribe to. Observe timeout/retry behaviour through their thrown errors (TimeoutError) and return values instead, not through the event stream.
Pass onEvent to any policy, or let the module wire the sinks for you. Combine several with combineSinks:
import { circuitBreaker, combineSinks, diagnosticsSink } from '@dudousxd/nestjs-resilience';
circuitBreaker({
key: 'payments',
store,
threshold: 5,
cooldownMs: 30_000,
onEvent: combineSinks(diagnosticsSink(), (event) => logger.warn(`resilience: ${event.type} ${event.key ?? ''}`)),
});Diagnostics
diagnosticsSink() routes each event to @dudousxd/nestjs-diagnostics on the aviary:resilience:<type> channel — so Telescope, OpenTelemetry, or any subscriber can observe circuit and failover activity. It's a no-op when diagnostics isn't installed.
ResilienceModule.forRoot({ emit: true }); // default — installs diagnosticsSink for youThe module wires this automatically; you only call diagnosticsSink() by hand when composing onEvent on a standalone policy.
Telescope dashboard
For a first-class view in Telescope — instead of generic diagnostic blobs — add @dudousxd/nestjs-resilience-telescope. It records every transition as a resilience entry and contributes a Resilience dashboard (open circuits, recent failovers, most-tripped circuits, and a table of recent transitions):
import { TelescopeModule } from '@dudousxd/nestjs-telescope';
import { nestjsResilienceTelescope } from '@dudousxd/nestjs-resilience-telescope';
TelescopeModule.forRoot({ extensions: [nestjsResilienceTelescope()] });It subscribes to the aviary:resilience:* channels, so it needs nothing beyond the default emit: true — and costs nothing until resilience emits. See the Telescope dashboard page for panels, options, and the exported building blocks.
Reacting in-app
To run logic when a transition happens — invalidate a cache when a circuit opens, page on repeated failovers — subscribe to the diagnostics channel directly. This needs nothing beyond diagnostics (already present whenever emit: true is on, the default), so there's no extra event library to install:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { getChannel, type DiagnosticEvent } from '@dudousxd/nestjs-diagnostics';
import type { ResilienceEvent } from '@dudousxd/nestjs-resilience';
@Injectable()
export class CircuitReactions implements OnModuleInit {
onModuleInit() {
getChannel('resilience', 'circuit-opened').subscribe((msg) => {
const event = (msg as DiagnosticEvent).payload as ResilienceEvent;
this.cache.invalidate(event.key);
});
}
}There's one channel per emitted ResilienceEventType under aviary:resilience:<type> — subscribe to each you care about: circuit-opened, circuit-closed, circuit-half-open, short-circuited, and failover.
Prefer raw onEvent sinks over the diagnostics channel? Pass any EventSink (or combineSinks(...)) to a policy — see Events above. Either way you stay on one event stream that observability tools read too.
Tenant-aware circuit keys
In a multi-tenant app you usually want a circuit per tenant — one tenant's failing provider shouldn't trip the breaker for everyone. tenantSuffix() reads the current tenant from @dudousxd/nestjs-context (via a shared Symbol, returning undefined when context isn't present), so you can fold it into the key:
import { circuitBreaker, tenantSuffix } from '@dudousxd/nestjs-resilience';
circuitBreaker({
key: `payments:${tenantSuffix() ?? 'global'}`,
store,
threshold: 5,
cooldownMs: 30_000,
});Because the key carries the tenant, a distributed store keeps each tenant's circuit isolated and fleet-wide at the same time.
Stores
Share circuit-breaker state across instances with a pluggable ResilienceStore — in-memory by default, or Redis / Postgres / SQLite adapters that coordinate the half-open probe atomically.
Telescope dashboard
Add @dudousxd/nestjs-resilience-telescope for a first-class Resilience dashboard in Telescope — open circuits, recent failovers, most-tripped keys, and a live table of transitions — recorded straight off the diagnostics channels.