@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 +21 -0
- package/README.md +103 -0
- package/dist/channel.d.ts +34 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +60 -0
- package/dist/channel.js.map +1 -0
- package/dist/context-accessor.d.ts +43 -0
- package/dist/context-accessor.d.ts.map +1 -0
- package/dist/context-accessor.js +34 -0
- package/dist/context-accessor.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +65 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +49 -0
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"}
|
package/dist/channel.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/registry.js
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|