Aviary

Getting Started

Install the package, emit diagnostic events, and correlate them with a trace id.

Install

pnpm add @dudousxd/nestjs-diagnostics

There's nothing to register — no module, no provider. The package is a set of functions over Node's diagnostics_channel.

Emitting events

Call emit(lib, event, payload) from your provider wherever something interesting happens:

import { Injectable } from '@nestjs/common';
import { emit } from '@dudousxd/nestjs-diagnostics';

@Injectable()
export class BillingService {
  async markInvoicePaid(invoiceId: string, amount: number) {
    // … your domain logic …
    emit('billing', 'invoice-paid', { invoiceId, amount });
  }
}
  • lib identifies the emitting library ('billing', 'authz', 'jobs'…).
  • event is the event within it ('invoice-paid', 'decision'…).
  • payload is your own data.

The event is published on the channel aviary:billing:invoice-paid.

Free when unobserved

emit builds and publishes the envelope only when the channel has subscribers (channel.hasSubscribers). A production process with no observer attached pays essentially nothing per call — so libraries can emit by default. emit also never throws: observability must never break the code path that produced the event.

Trace correlation

If your app uses @dudousxd/nestjs-context, register its accessor once and every emit auto-fills traceId from the active request:

import { setContextAccessor, CONTEXT_ACCESSOR } from '@dudousxd/nestjs-diagnostics';

// e.g. in a Nest module, after resolving the optional CONTEXT_ACCESSOR provider:
setContextAccessor(accessor);

CONTEXT_ACCESSOR is Symbol.for('@dudousxd/nestjs-context:accessor') — the same token nestjs-context publishes under, so any object structurally matching the accessor works. nestjs-context is an optional peer; it is never imported here.

Need an explicit id instead? Pass it in opts — e.g. from a method that already holds a correlation id:

@Injectable()
export class BillingService {
  async markInvoicePaid(payload: InvoicePaid, traceId: string) {
    emit('billing', 'invoice-paid', payload, { traceId });
  }
}

Timing an operation

If the event describes something that took measurable time — an outbound call, a query, a job run — stamp the wall-clock duration onto it with opts.durationMs. emit copies it onto the envelope as durationMs, letting downstream consumers build duration histograms instead of only counting events:

import { Injectable } from '@nestjs/common';
import { emit } from '@dudousxd/nestjs-diagnostics';

@Injectable()
export class AiService {
  constructor(private readonly client: ChatClient) {}

  async chat(prompt: string) {
    const start = performance.now();
    const reply = await this.client.complete(prompt);
    emit('ai', 'chat-request', { model: 'gpt-4o', tokens: reply.tokens }, {
      durationMs: performance.now() - start,
    });
    return reply;
  }
}

Optional by design

durationMs is set on the envelope only when you pass it — omit it for events that aren't tied to a timed operation, and the key won't appear at all. There's no need to send 0 or a placeholder.

API surface

ExportDescription
emit(lib, event, payload, opts?)Build + publish a DiagnosticEvent on aviary:<lib>:<event> (only when subscribed). opts accepts traceId, durationMs, and sample.
channelName(lib, event)The aviary:<lib>:<event> string.
getChannel(lib, event)The memoized diagnostics_channel for a pair (also registers its name).
CHANNEL_PREFIX'aviary'.
registeredChannels()Snapshot of every registered channel name.
onChannelRegistered(cb)Notified once per future channel registration; returns an unsubscribe.
setContextAccessor(accessor | null)Register the accessor emit reads traceId from.
CONTEXT_ACCESSORShared DI token for the optional context accessor.

The registeredChannels / onChannelRegistered pair is what makes generic, wildcard-style observing possible — see Consumers.

On this page