Getting Started
Install the package, emit diagnostic events, and correlate them with a trace id.
Install
pnpm add @dudousxd/nestjs-diagnosticsThere'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 });
}
}libidentifies the emitting library ('billing','authz','jobs'…).eventis the event within it ('invoice-paid','decision'…).payloadis 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
| Export | Description |
|---|---|
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_ACCESSOR | Shared DI token for the optional context accessor. |
The registeredChannels / onChannelRegistered pair is what makes generic, wildcard-style observing possible — see Consumers.