@dudousxd/nestjs-diagnostics 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # @dudousxd/nestjs-diagnostics
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`fce932d`](https://github.com/DavideCarvalho/nestjs-diagnostics/commit/fce932df74ba25d47d9b41b3dc03b61674bd976f) - Initial release.
8
+
9
+ `@dudousxd/nestjs-diagnostics` — a standard convention for `@dudousxd/nestjs-*`
10
+ libraries to emit observability events over `node:diagnostics_channel`. Channels
11
+ are named `aviary:<lib>:<event>`; `emit(lib, event, payload, opts?)` builds a
12
+ `DiagnosticEvent` envelope (`ts`, `lib`, `event`, optional `traceId`, `payload`)
13
+ and publishes only when the channel has subscribers. A process-wide registry
14
+ (`registeredChannels()` + `onChannelRegistered()`) makes every channel
15
+ discoverable, since `diagnostics_channel` has no wildcard subscription. An
16
+ optional context accessor (`setContextAccessor`) auto-fills `traceId`.
17
+
18
+ `@dudousxd/nestjs-diagnostics-telescope` — a `nestjs-telescope` extension whose
19
+ single generic `DiagnosticWatcher` subscribes to every registered channel
20
+ (current + future) and records each event as a `diagnostic` entry, with a
21
+ "Diagnostics" dashboard page.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @dudousxd/nestjs-diagnostics
2
+
3
+ A tiny, standard convention for `@dudousxd/nestjs-*` libraries to emit
4
+ observability events over Node's built-in
5
+ [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) — with
6
+ zero overhead when nobody is listening, and a single generic
7
+ [Telescope](https://www.npmjs.com/package/@dudousxd/nestjs-telescope) watcher
8
+ (`@dudousxd/nestjs-diagnostics-telescope`) that records them all.
9
+
10
+ ## The convention
11
+
12
+ Every event flows over a channel named:
13
+
14
+ ```
15
+ aviary:<lib>:<event>
16
+ ```
17
+
18
+ - `aviary` is the fixed prefix for this family of libraries.
19
+ - `<lib>` identifies the emitting library, e.g. `billing`, `audit`, `jobs`.
20
+ - `<event>` is the event within that library, e.g. `invoice-paid`.
21
+
22
+ Each publish carries a `DiagnosticEvent` envelope:
23
+
24
+ ```ts
25
+ interface DiagnosticEvent<TPayload = unknown> {
26
+ ts: number; // Date.now() at publish time
27
+ lib: string; // the <lib>
28
+ event: string; // the <event>
29
+ traceId?: string; // auto-filled from a context accessor when available
30
+ payload: TPayload; // your library-defined data
31
+ }
32
+ ```
33
+
34
+ ## Emitting
35
+
36
+ ```ts
37
+ import { emit } from '@dudousxd/nestjs-diagnostics';
38
+
39
+ emit('billing', 'invoice-paid', { invoiceId: 'inv_123', amount: 4200 });
40
+ ```
41
+
42
+ `emit` builds and publishes the envelope **only when the channel has
43
+ subscribers** (`channel.hasSubscribers`), so a production process with no
44
+ observer attached pays essentially nothing per call. It never throws —
45
+ observability must not break the emitting code path.
46
+
47
+ Need an explicit trace id? Pass it in `opts`:
48
+
49
+ ```ts
50
+ emit('billing', 'invoice-paid', payload, { traceId: req.id });
51
+ ```
52
+
53
+ ## Trace correlation
54
+
55
+ If your app uses `@dudousxd/nestjs-context` (an optional peer — never imported
56
+ here), register its accessor once and every `emit` will auto-fill `traceId`:
57
+
58
+ ```ts
59
+ import { setContextAccessor, CONTEXT_ACCESSOR } from '@dudousxd/nestjs-diagnostics';
60
+
61
+ // e.g. in a Nest module after resolving the optional CONTEXT_ACCESSOR provider:
62
+ setContextAccessor(accessor);
63
+ ```
64
+
65
+ `CONTEXT_ACCESSOR` is `Symbol.for('@dudousxd/nestjs-context:accessor')` — the
66
+ same token nestjs-context publishes under. Any object structurally matching the
67
+ `ContextAccessor` interface works.
68
+
69
+ ## Observing (the registry)
70
+
71
+ `diagnostics_channel` has **no wildcard subscription** — you can only subscribe
72
+ to a channel by exact name. So this package keeps a registry of every
73
+ `aviary:<lib>:<event>` channel it has created or emitted on:
74
+
75
+ ```ts
76
+ import { registeredChannels, onChannelRegistered } from '@dudousxd/nestjs-diagnostics';
77
+
78
+ // subscribe to all current channels…
79
+ for (const name of registeredChannels()) subscribe(name);
80
+ // …and any registered in the future
81
+ const off = onChannelRegistered(subscribe);
82
+ ```
83
+
84
+ This is exactly how `@dudousxd/nestjs-diagnostics-telescope`'s single generic
85
+ watcher records every diagnostic event in the Telescope dashboard.
86
+
87
+ ## API
88
+
89
+ | Export | Description |
90
+ | --- | --- |
91
+ | `emit(lib, event, payload, opts?)` | Build + publish a `DiagnosticEvent` on `aviary:<lib>:<event>` (only when subscribed). |
92
+ | `getChannel(lib, event)` | The memoized `diagnostics_channel` for a pair (also registers its name). |
93
+ | `channelName(lib, event)` | The `aviary:<lib>:<event>` string. |
94
+ | `CHANNEL_PREFIX` | `'aviary'`. |
95
+ | `registeredChannels()` | Snapshot of every registered channel name. |
96
+ | `onChannelRegistered(cb)` | Notified once per future channel registration; returns an unsubscribe. |
97
+ | `setContextAccessor(accessor \| null)` | Register the accessor `emit` reads `traceId` from. |
98
+ | `CONTEXT_ACCESSOR` | Shared DI token for the optional context accessor. |
99
+ | `DiagnosticEvent`, `EmitOptions`, `ContextAccessor` | Types. |
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,34 @@
1
+ import { type Channel } from 'node:diagnostics_channel';
2
+ import type { EmitOptions } from './types.js';
3
+ /** The prefix all channels created by this convention share. */
4
+ export declare const CHANNEL_PREFIX = "aviary";
5
+ /**
6
+ * The channel name for a `<lib>`/`<event>` pair: `aviary:<lib>:<event>`. This is
7
+ * the cross-repo wire contract — keep it identical on producer and observer.
8
+ */
9
+ export declare function channelName(lib: string, event: string): string;
10
+ /**
11
+ * The memoized `node:diagnostics_channel` for a `<lib>`/`<event>` pair. Node
12
+ * returns the same {@link Channel} object for the same name, so reading
13
+ * `.hasSubscribers` is the cheap gate before building an envelope. Touching the
14
+ * channel also records its name in the {@link registerChannel registry} so a
15
+ * generic observer can discover it.
16
+ */
17
+ export declare function getChannel(lib: string, event: string): Channel;
18
+ /**
19
+ * Emit a diagnostics event on `aviary:<lib>:<event>`.
20
+ *
21
+ * - The envelope's `ts` is `Date.now()` evaluated *inside* this call (never at
22
+ * module load).
23
+ * - `traceId` is taken from `opts.traceId` if given, else auto-filled from the
24
+ * registered context accessor (if resolvable), else left undefined.
25
+ * - The envelope is built and published ONLY when the channel `hasSubscribers`,
26
+ * so emitting is effectively free when nothing is listening.
27
+ * - Never throws: emitting observability must not break the caller.
28
+ *
29
+ * ```ts
30
+ * emit('billing', 'invoice-paid', { invoiceId: 'inv_123', amount: 4200 });
31
+ * ```
32
+ */
33
+ export declare function emit(lib: string, event: string, payload: unknown, opts?: EmitOptions): void;
34
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAA4B,EAAE,KAAK,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAG7E,OAAO,KAAK,EAAmB,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/D,gEAAgE;AAChE,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,IAAI,CAgB3F"}
@@ -0,0 +1,60 @@
1
+ import diagnostics_channel from 'node:diagnostics_channel';
2
+ import { resolveTraceId } from './context-accessor.js';
3
+ import { registerChannel } from './registry.js';
4
+ /** The prefix all channels created by this convention share. */
5
+ export const CHANNEL_PREFIX = 'aviary';
6
+ /**
7
+ * The channel name for a `<lib>`/`<event>` pair: `aviary:<lib>:<event>`. This is
8
+ * the cross-repo wire contract — keep it identical on producer and observer.
9
+ */
10
+ export function channelName(lib, event) {
11
+ return `${CHANNEL_PREFIX}:${lib}:${event}`;
12
+ }
13
+ /**
14
+ * The memoized `node:diagnostics_channel` for a `<lib>`/`<event>` pair. Node
15
+ * returns the same {@link Channel} object for the same name, so reading
16
+ * `.hasSubscribers` is the cheap gate before building an envelope. Touching the
17
+ * channel also records its name in the {@link registerChannel registry} so a
18
+ * generic observer can discover it.
19
+ */
20
+ export function getChannel(lib, event) {
21
+ const name = channelName(lib, event);
22
+ const channel = diagnostics_channel.channel(name);
23
+ registerChannel(name);
24
+ return channel;
25
+ }
26
+ /**
27
+ * Emit a diagnostics event on `aviary:<lib>:<event>`.
28
+ *
29
+ * - The envelope's `ts` is `Date.now()` evaluated *inside* this call (never at
30
+ * module load).
31
+ * - `traceId` is taken from `opts.traceId` if given, else auto-filled from the
32
+ * registered context accessor (if resolvable), else left undefined.
33
+ * - The envelope is built and published ONLY when the channel `hasSubscribers`,
34
+ * so emitting is effectively free when nothing is listening.
35
+ * - Never throws: emitting observability must not break the caller.
36
+ *
37
+ * ```ts
38
+ * emit('billing', 'invoice-paid', { invoiceId: 'inv_123', amount: 4200 });
39
+ * ```
40
+ */
41
+ export function emit(lib, event, payload, opts) {
42
+ const channel = getChannel(lib, event);
43
+ if (!channel.hasSubscribers)
44
+ return;
45
+ try {
46
+ const traceId = opts?.traceId ?? resolveTraceId();
47
+ const envelope = {
48
+ ts: Date.now(),
49
+ lib,
50
+ event,
51
+ ...(traceId !== undefined ? { traceId } : {}),
52
+ payload,
53
+ };
54
+ channel.publish(envelope);
55
+ }
56
+ catch {
57
+ // Observability must never break the emitting code path.
58
+ }
59
+ }
60
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAqC,MAAM,0BAA0B,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,gEAAgE;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,KAAa;IACpD,OAAO,GAAG,cAAc,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa;IACnD,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,IAAI,CAAC,GAAW,EAAE,KAAa,EAAE,OAAgB,EAAE,IAAkB;IACnF,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO,CAAC,cAAc;QAAE,OAAO;IACpC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAoB;YAChC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,GAAG;YACH,KAAK;YACL,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,OAAO;SACR,CAAC;QACF,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,yDAAyD;IAC3D,CAAC;AACH,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Local, structural mirror of `@dudousxd/nestjs-context`'s public accessor
3
+ * (`packages/core/src/accessor.ts`).
4
+ *
5
+ * We deliberately do NOT import nestjs-context (it is an OPTIONAL peer). Instead
6
+ * we declare the same shape here; any object that structurally satisfies this
7
+ * interface — including nestjs-context's real accessor — can be registered via
8
+ * {@link setContextAccessor} so {@link emit} can auto-fill `traceId`.
9
+ *
10
+ * Kept byte-aligned with nestjs-context's `ContextAccessor`: `traceId()` /
11
+ * `tenantId()` / `userRef()` / `get()` are all present so the structural match
12
+ * stays exact and a future use of any of them is type-safe.
13
+ */
14
+ export interface UserRef {
15
+ type: string;
16
+ id: string | number;
17
+ }
18
+ /** Opaque shape of the context store. diagnostics never reads it; mirrors the upstream surface. */
19
+ export type ContextStore = Record<string, unknown>;
20
+ export interface ContextAccessor {
21
+ /** Trace id for the current request, or `undefined` when unavailable. */
22
+ traceId(): string | undefined;
23
+ /** Current tenant id, or `undefined` when no multi-tenant context is populated. */
24
+ tenantId(): string | undefined;
25
+ /** Reference to the current user, or `undefined` when unauthenticated. */
26
+ userRef(): UserRef | undefined;
27
+ /** The raw context store for the current request, or `undefined`. */
28
+ get(): ContextStore | undefined;
29
+ }
30
+ /**
31
+ * The shared token nestjs-context publishes its accessor under. Exposed so a
32
+ * Nest app can `{ provide: CONTEXT_ACCESSOR, useExisting: ... }` and a consumer
33
+ * can `@Inject(CONTEXT_ACCESSOR) @Optional()` it — symmetric with how the rest
34
+ * of the `@dudousxd/nestjs-*` family wires the optional context peer.
35
+ */
36
+ export declare const CONTEXT_ACCESSOR: unique symbol;
37
+ /** Register (or clear, with `null`) the accessor {@link emit} reads `traceId` from. */
38
+ export declare function setContextAccessor(next: ContextAccessor | null): void;
39
+ /** The currently registered accessor, or `null`. */
40
+ export declare function getContextAccessor(): ContextAccessor | null;
41
+ /** Resolve the current trace id from the registered accessor, never throwing. */
42
+ export declare function resolveTraceId(): string | undefined;
43
+ //# sourceMappingURL=context-accessor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-accessor.d.ts","sourceRoot":"","sources":["../src/context-accessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB;AAED,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,OAAO,IAAI,MAAM,GAAG,SAAS,CAAC;IAC9B,mFAAmF;IACnF,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAC;IAC/B,0EAA0E;IAC1E,OAAO,IAAI,OAAO,GAAG,SAAS,CAAC;IAC/B,qEAAqE;IACrE,GAAG,IAAI,YAAY,GAAG,SAAS,CAAC;CACjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,eAAkD,CAAC;AAUhF,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAErE;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAE3D;AAED,iFAAiF;AACjF,wBAAgB,cAAc,IAAI,MAAM,GAAG,SAAS,CAOnD"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The shared token nestjs-context publishes its accessor under. Exposed so a
3
+ * Nest app can `{ provide: CONTEXT_ACCESSOR, useExisting: ... }` and a consumer
4
+ * can `@Inject(CONTEXT_ACCESSOR) @Optional()` it — symmetric with how the rest
5
+ * of the `@dudousxd/nestjs-*` family wires the optional context peer.
6
+ */
7
+ export const CONTEXT_ACCESSOR = Symbol.for('@dudousxd/nestjs-context:accessor');
8
+ /**
9
+ * Module-level accessor used by {@link emit} to auto-fill `traceId`. `null` until
10
+ * an app calls {@link setContextAccessor} (e.g. from a Nest module's `onModuleInit`
11
+ * after resolving the optional {@link CONTEXT_ACCESSOR} provider). Kept out of
12
+ * the hot path: when unset, `emit` simply leaves `traceId` undefined.
13
+ */
14
+ let accessor = null;
15
+ /** Register (or clear, with `null`) the accessor {@link emit} reads `traceId` from. */
16
+ export function setContextAccessor(next) {
17
+ accessor = next;
18
+ }
19
+ /** The currently registered accessor, or `null`. */
20
+ export function getContextAccessor() {
21
+ return accessor;
22
+ }
23
+ /** Resolve the current trace id from the registered accessor, never throwing. */
24
+ export function resolveTraceId() {
25
+ if (accessor == null)
26
+ return undefined;
27
+ try {
28
+ return accessor.traceId();
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ //# sourceMappingURL=context-accessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-accessor.js","sourceRoot":"","sources":["../src/context-accessor.ts"],"names":[],"mappings":"AAgCA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,IAAI,QAAQ,GAA2B,IAAI,CAAC;AAE5C,uFAAuF;AACvF,MAAM,UAAU,kBAAkB,CAAC,IAA4B;IAC7D,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,kBAAkB;IAChC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc;IAC5B,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACvC,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { CHANNEL_PREFIX, channelName, emit, getChannel } from './channel.js';
2
+ export { CONTEXT_ACCESSOR, type ContextAccessor, type ContextStore, getContextAccessor, resolveTraceId, setContextAccessor, type UserRef, } from './context-accessor.js';
3
+ export { onChannelRegistered, registerChannel, registeredChannels, resetRegistry, } from './registry.js';
4
+ export type { DiagnosticEvent, EmitOptions } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,EACL,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,KAAK,OAAO,GACb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { CHANNEL_PREFIX, channelName, emit, getChannel } from './channel.js';
2
+ export { CONTEXT_ACCESSOR, getContextAccessor, resolveTraceId, setContextAccessor, } from './context-accessor.js';
3
+ export { onChannelRegistered, registerChannel, registeredChannels, resetRegistry, } from './registry.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,EACL,gBAAgB,EAGhB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,GAEnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,aAAa,GACd,MAAM,eAAe,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A process-wide registry of every `aviary:<lib>:<event>` channel name created or
3
+ * emitted through this package.
4
+ *
5
+ * `node:diagnostics_channel` has no wildcard subscription: you can only subscribe
6
+ * to a channel by its exact name. A generic observer (the Telescope watcher) that
7
+ * wants to record *all* diagnostics therefore needs to (a) subscribe to every
8
+ * channel that exists now and (b) be told when a new one appears. This registry
9
+ * provides both: {@link registeredChannels} for the current set and
10
+ * {@link onChannelRegistered} for future additions.
11
+ */
12
+ /**
13
+ * Record a channel name, notifying {@link onChannelRegistered} listeners the first
14
+ * time it is seen. Idempotent: re-registering an existing name is a no-op.
15
+ * Called by {@link getChannel}/{@link emit} so every channel touched through this
16
+ * package is discoverable.
17
+ */
18
+ export declare function registerChannel(name: string): void;
19
+ /** A snapshot of every `aviary:<lib>:<event>` channel registered so far. */
20
+ export declare function registeredChannels(): string[];
21
+ /**
22
+ * Subscribe to channel registrations. The callback fires once per channel name
23
+ * registered *after* this call. Returns an unsubscribe function.
24
+ *
25
+ * Note: this does NOT replay existing channels — a generic watcher should pair
26
+ * it with {@link registeredChannels} to cover current + future names:
27
+ *
28
+ * ```ts
29
+ * for (const name of registeredChannels()) subscribe(name);
30
+ * const off = onChannelRegistered(subscribe);
31
+ * ```
32
+ */
33
+ export declare function onChannelRegistered(cb: (name: string) => void): () => void;
34
+ /**
35
+ * Test-only: forget every registered channel and listener. Not part of the
36
+ * public contract — exposed so suites can assert registration in isolation.
37
+ */
38
+ export declare function resetRegistry(): void;
39
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAUlD;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAK1E;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAGpC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A process-wide registry of every `aviary:<lib>:<event>` channel name created or
3
+ * emitted through this package.
4
+ *
5
+ * `node:diagnostics_channel` has no wildcard subscription: you can only subscribe
6
+ * to a channel by its exact name. A generic observer (the Telescope watcher) that
7
+ * wants to record *all* diagnostics therefore needs to (a) subscribe to every
8
+ * channel that exists now and (b) be told when a new one appears. This registry
9
+ * provides both: {@link registeredChannels} for the current set and
10
+ * {@link onChannelRegistered} for future additions.
11
+ */
12
+ /** Every channel name seen so far, in insertion order. */
13
+ const channels = new Set();
14
+ /** Listeners notified once per newly-registered channel name. */
15
+ const listeners = new Set();
16
+ /**
17
+ * Record a channel name, notifying {@link onChannelRegistered} listeners the first
18
+ * time it is seen. Idempotent: re-registering an existing name is a no-op.
19
+ * Called by {@link getChannel}/{@link emit} so every channel touched through this
20
+ * package is discoverable.
21
+ */
22
+ export function registerChannel(name) {
23
+ if (channels.has(name))
24
+ return;
25
+ channels.add(name);
26
+ for (const listener of listeners) {
27
+ try {
28
+ listener(name);
29
+ }
30
+ catch {
31
+ // A misbehaving observer must never break event emission.
32
+ }
33
+ }
34
+ }
35
+ /** A snapshot of every `aviary:<lib>:<event>` channel registered so far. */
36
+ export function registeredChannels() {
37
+ return [...channels];
38
+ }
39
+ /**
40
+ * Subscribe to channel registrations. The callback fires once per channel name
41
+ * registered *after* this call. Returns an unsubscribe function.
42
+ *
43
+ * Note: this does NOT replay existing channels — a generic watcher should pair
44
+ * it with {@link registeredChannels} to cover current + future names:
45
+ *
46
+ * ```ts
47
+ * for (const name of registeredChannels()) subscribe(name);
48
+ * const off = onChannelRegistered(subscribe);
49
+ * ```
50
+ */
51
+ export function onChannelRegistered(cb) {
52
+ listeners.add(cb);
53
+ return () => {
54
+ listeners.delete(cb);
55
+ };
56
+ }
57
+ /**
58
+ * Test-only: forget every registered channel and listener. Not part of the
59
+ * public contract — exposed so suites can assert registration in isolation.
60
+ */
61
+ export function resetRegistry() {
62
+ channels.clear();
63
+ listeners.clear();
64
+ }
65
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,0DAA0D;AAC1D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;AAEnC,iEAAiE;AACjE,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IAC/B,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;QAC5D,CAAC;IACH,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,kBAAkB;IAChC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAA0B;IAC5D,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The envelope every `@dudousxd/nestjs-*` library publishes over
3
+ * `node:diagnostics_channel`. It is the cross-repo wire contract: the generic
4
+ * Telescope watcher (and any other observer) reads exactly these fields, so keep
5
+ * the shape stable. The `payload` is opaque — its meaning is owned by the
6
+ * emitting library and identified by `lib` + `event`.
7
+ */
8
+ export interface DiagnosticEvent<TPayload = unknown> {
9
+ /** Epoch millis when the event was emitted (`Date.now()` at publish time). */
10
+ ts: number;
11
+ /** The emitting library, e.g. `'billing'` — the `<lib>` in `aviary:<lib>:<event>`. */
12
+ lib: string;
13
+ /** The event name, e.g. `'invoice-paid'` — the `<event>` in `aviary:<lib>:<event>`. */
14
+ event: string;
15
+ /** Trace id for the current request, when resolvable from a context accessor. */
16
+ traceId?: string;
17
+ /** The library-defined payload. Opaque to this package and to observers. */
18
+ payload: TPayload;
19
+ }
20
+ /** Optional per-emit overrides. */
21
+ export interface EmitOptions {
22
+ /**
23
+ * An explicit trace id for this event. When provided it wins over the
24
+ * registered {@link ContextAccessor}. Omit to auto-fill from the accessor (if
25
+ * any), or leave `traceId` undefined.
26
+ */
27
+ traceId?: string;
28
+ }
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,QAAQ,GAAG,OAAO;IACjD,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,sFAAsF;IACtF,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,QAAQ,CAAC;CACnB;AAED,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@dudousxd/nestjs-diagnostics",
3
+ "version": "0.2.0",
4
+ "description": "A standard convention for @dudousxd/nestjs-* libs to emit observability events over node:diagnostics_channel.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/DavideCarvalho/nestjs-diagnostics.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "author": "Davi Carvalho <davi@goflip.ai>",
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "typescript": "^5.4.0",
30
+ "vitest": "^3.0.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "keywords": [
36
+ "nestjs",
37
+ "diagnostics",
38
+ "diagnostics-channel",
39
+ "observability",
40
+ "telemetry",
41
+ "events"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "test": "vitest run --passWithNoTests",
46
+ "test:watch": "vitest",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit"
48
+ }
49
+ }