Aviary

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:

type ResilienceEventType =
  | 'circuit-opened' | 'circuit-closed' | 'circuit-half-open'
  | 'short-circuited' | 'failover' | 'timeout' | 'retry';

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;

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 you

The 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.

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 ResilienceEventType under aviary:resilience:<type> — subscribe to each you care about (circuit-opened, short-circuited, 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.

On this page