@dudousxd/nestjs-diagnostics 0.5.0 → 0.6.1
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 +12 -0
- package/README.md +13 -3
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +3 -0
- package/dist/channel.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/skills/capabilities/SKILL.md +160 -0
- package/skills/emit-diagnostics/SKILL.md +210 -0
- package/skills/observe-channels/SKILL.md +164 -0
- package/skills/react-with-on-diagnostic/SKILL.md +163 -0
- package/skills/trace-spans/SKILL.md +156 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @dudousxd/nestjs-diagnostics
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#14](https://github.com/DavideCarvalho/nestjs-diagnostics/pull/14) [`cced7fc`](https://github.com/DavideCarvalho/nestjs-diagnostics/commit/cced7fc843c84eaf7b51686321294355bf2dd346) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Ship TanStack Intent agent skills (SKILL.md) inside the package.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`16bd3a5`](https://github.com/DavideCarvalho/nestjs-diagnostics/commit/16bd3a5ae975edb8e8b9b8ea59d52e7a9a0b470f) - Diagnostics events can carry an optional `durationMs`. `emit(lib, event, payload, { durationMs })` stamps a wall-clock duration onto the envelope, and the generic Telescope watcher forwards it to the recorded entry's `durationMs`. This lets an observer (e.g. the Telescope OTel exporter) turn an `aviary:<lib>:<event>` stream into a **duration histogram** (p95/p99) instead of only a counter — so latency metrics can move onto the diagnostics→telescope path without losing their distribution.
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -33,10 +33,19 @@ interface DiagnosticEvent<TPayload = unknown> {
|
|
|
33
33
|
|
|
34
34
|
## Emitting
|
|
35
35
|
|
|
36
|
+
Call `emit` from your provider wherever something interesting happens:
|
|
37
|
+
|
|
36
38
|
```ts
|
|
39
|
+
import { Injectable } from '@nestjs/common';
|
|
37
40
|
import { emit } from '@dudousxd/nestjs-diagnostics';
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class BillingService {
|
|
44
|
+
async markInvoicePaid(invoiceId: string, amount: number) {
|
|
45
|
+
// … your domain logic …
|
|
46
|
+
emit('billing', 'invoice-paid', { invoiceId, amount });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
```
|
|
41
50
|
|
|
42
51
|
`emit` builds and publishes the envelope **only when the channel has
|
|
@@ -44,10 +53,11 @@ subscribers** (`channel.hasSubscribers`), so a production process with no
|
|
|
44
53
|
observer attached pays essentially nothing per call. It never throws —
|
|
45
54
|
observability must not break the emitting code path.
|
|
46
55
|
|
|
47
|
-
Need an explicit trace id? Pass it in `opts
|
|
56
|
+
Need an explicit trace id? Pass it in `opts` — e.g. inside a handler that
|
|
57
|
+
already holds a correlation id:
|
|
48
58
|
|
|
49
59
|
```ts
|
|
50
|
-
emit('billing', 'invoice-paid', payload, { traceId
|
|
60
|
+
emit('billing', 'invoice-paid', payload, { traceId });
|
|
51
61
|
```
|
|
52
62
|
|
|
53
63
|
## Trace correlation
|
package/dist/channel.d.ts.map
CHANGED
|
@@ -1 +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,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1F,gEAAgE;AAChE,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAqBD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAc9D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,IAAI,CAAC,IAAI,SAAS,KAAK,EAAE,MAAM,SAAS,OAAO,CAAC,IAAI,CAAC,EACnE,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,EAChC,IAAI,CAAC,EAAE,WAAW,GACjB,IAAI,
|
|
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,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1F,gEAAgE;AAChE,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAqBD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAc9D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,IAAI,CAAC,IAAI,SAAS,KAAK,EAAE,MAAM,SAAS,OAAO,CAAC,IAAI,CAAC,EACnE,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,EAChC,IAAI,CAAC,EAAE,WAAW,GACjB,IAAI,CAuBN"}
|
package/dist/channel.js
CHANGED
|
@@ -105,6 +105,9 @@ export function emit(lib, event, payload, opts) {
|
|
|
105
105
|
event,
|
|
106
106
|
traceId,
|
|
107
107
|
payload,
|
|
108
|
+
// Only set the key when a duration was provided — omitting it keeps the
|
|
109
|
+
// envelope shape monomorphic (no undefined-valued key on common events).
|
|
110
|
+
...(opts?.durationMs !== undefined && { durationMs: opts.durationMs }),
|
|
108
111
|
};
|
|
109
112
|
channel.publish(envelope);
|
|
110
113
|
}
|
package/dist/channel.js.map
CHANGED
|
@@ -1 +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,eAAe,EAAE,MAAM,eAAe,CAAC;AAGjE,gEAAgE;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,KAAa;IACpD,OAAO,GAAG,cAAc,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE7D,8EAA8E;AAC9E,6EAA6E;AAC7E,eAAe,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa;IACnD,IAAI,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;IAC1C,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,IAAI,GAAG,EAAmB,CAAC;QACrC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,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,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,IAAI,CAClB,GAAS,EACT,KAAa,EACb,OAAgC,EAChC,IAAkB;IAElB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO,CAAC,cAAc;QAAE,OAAO;IACpC,IAAI,CAAC;QACH,6EAA6E;QAC7E,4EAA4E;QAC5E,IAAI,IAAI,EAAE,MAAM,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,OAAO;QACzD,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAoB;YAChC,CAAC,EAAE,cAAc;YACjB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,GAAG;YACH,KAAK;YACL,OAAO;YACP,OAAO;
|
|
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,eAAe,EAAE,MAAM,eAAe,CAAC;AAGjE,gEAAgE;AAChE,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,KAAa;IACpD,OAAO,GAAG,cAAc,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE7D,8EAA8E;AAC9E,6EAA6E;AAC7E,eAAe,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAa;IACnD,IAAI,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;IAC1C,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,IAAI,GAAG,EAAmB,CAAC;QACrC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,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,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,IAAI,CAClB,GAAS,EACT,KAAa,EACb,OAAgC,EAChC,IAAkB;IAElB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO,CAAC,cAAc;QAAE,OAAO;IACpC,IAAI,CAAC;QACH,6EAA6E;QAC7E,4EAA4E;QAC5E,IAAI,IAAI,EAAE,MAAM,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,OAAO;QACzD,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAoB;YAChC,CAAC,EAAE,cAAc;YACjB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,GAAG;YACH,KAAK;YACL,OAAO;YACP,OAAO;YACP,wEAAwE;YACxE,yEAAyE;YACzE,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;SACvE,CAAC;QACF,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,yDAAyD;IAC3D,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface DiagnosticEvent<TPayload = unknown> {
|
|
|
28
28
|
traceId?: string | undefined;
|
|
29
29
|
/** The library-defined payload. Opaque to this package and to observers. */
|
|
30
30
|
payload: TPayload;
|
|
31
|
+
/**
|
|
32
|
+
* Wall-clock duration of the operation this event describes, in milliseconds,
|
|
33
|
+
* when known. Lets observers build duration histograms (e.g. OTel histogram
|
|
34
|
+
* instruments) instead of only counters. Omit when the event is not tied to a
|
|
35
|
+
* timed operation. Stamped from {@link EmitOptions.durationMs} by `emit`.
|
|
36
|
+
*/
|
|
37
|
+
durationMs?: number;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* The optional, consumer-augmented typed channel registry: a `lib → event →
|
|
@@ -156,6 +163,13 @@ export interface EmitOptions {
|
|
|
156
163
|
* cheap probabilistic predicate, e.g. `{ sample: () => Math.random() < 0.1 }`.
|
|
157
164
|
*/
|
|
158
165
|
sample?: () => boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Wall-clock duration of the operation this event describes, in milliseconds.
|
|
168
|
+
* When provided, `emit` stamps it onto the {@link DiagnosticEvent} envelope as
|
|
169
|
+
* `durationMs`, letting downstream observers (e.g. the Telescope OTel exporter)
|
|
170
|
+
* build duration histograms instead of only counters.
|
|
171
|
+
*/
|
|
172
|
+
durationMs?: number;
|
|
159
173
|
}
|
|
160
174
|
/** Optional per-`trace` overrides. */
|
|
161
175
|
export interface TraceOptions {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +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;;;;;;OAMG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,sFAAsF;IACtF,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,4EAA4E;IAC5E,OAAO,EAAE,QAAQ,CAAC;
|
|
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;;;;;;OAMG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,sFAAsF;IACtF,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,4EAA4E;IAC5E,OAAO,EAAE,QAAQ,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,eAAe;CAAG;AAEnC;;;;;GAKG;AACH,KAAK,WAAW,GAAG,MAAM,GAAG,EAAE,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,WAAW,CAAC;AAEnE;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,MAAM,eAAe,GACzE,CAAC,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,WAAW,GACpD,MAAM,CAAC;AAEX;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CACnB,IAAI,SAAS,MAAM,EACnB,MAAM,SAAS,MAAM,IACnB,IAAI,SAAS,MAAM,eAAe,GAClC,MAAM,SAAS,MAAM,eAAe,CAAC,IAAI,CAAC,GACxC,eAAe,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAC7B,OAAO,GACT,OAAO,CAAC;AAEZ;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,CAAC;AAE9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,SAAS,CAAC,QAAQ,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO;IAC9D;;;;;OAKG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,iEAAiE;IACjE,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-diagnostics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A standard convention for @dudousxd/nestjs-* libs to emit observability events over node:diagnostics_channel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"dist/",
|
|
45
45
|
"README.md",
|
|
46
|
-
"CHANGELOG.md"
|
|
46
|
+
"CHANGELOG.md",
|
|
47
|
+
"skills"
|
|
47
48
|
],
|
|
48
49
|
"//sideEffects": "NOT false: registry.ts and context-accessor.ts register process-global state on globalThis via Symbol.for AT IMPORT TIME (the cross-copy-stable channel registry + context accessor), and channel.ts/trace.ts register a reset hook at module load. A bundler told sideEffects:false could tree-shake these initializers away, breaking cross-copy global stability. The array lists exactly the side-effectful modules (matching both src and published dist paths); pure modules (index, types) stay tree-shakeable.",
|
|
49
50
|
"sideEffects": [
|
|
@@ -75,7 +76,8 @@
|
|
|
75
76
|
"diagnostics-channel",
|
|
76
77
|
"observability",
|
|
77
78
|
"telemetry",
|
|
78
|
-
"events"
|
|
79
|
+
"events",
|
|
80
|
+
"tanstack-intent"
|
|
79
81
|
],
|
|
80
82
|
"scripts": {
|
|
81
83
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capabilities
|
|
3
|
+
description: Share cross-library DI tokens via the capability half of the @dudousxd/nestjs-diagnostics protocol. capability(lib, name) mints a Symbol.for('@dudousxd/nestjs-<lib>:<name>') token producer and consumer resolve identically without importing each other; InjectCapability(lib, name) from /nestjs optionally injects a peer's capability; assertCapabilityNaming guards naming drift in contract tests; CapabilityRegistry/CapabilityOf give typed tokens via declaration merging. Mirrors channelName for events.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
type: core
|
|
7
|
+
library: "@dudousxd/nestjs-diagnostics"
|
|
8
|
+
library_version: 0.6.0
|
|
9
|
+
framework: nestjs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Cross-library capabilities (DI tokens)
|
|
13
|
+
|
|
14
|
+
`@dudousxd/nestjs-diagnostics` carries two halves of one ecosystem protocol: the
|
|
15
|
+
**event** transport (`emit` / channels) and **capabilities** — stable DI tokens
|
|
16
|
+
two `@dudousxd/nestjs-*` libraries share **without importing each other**. A
|
|
17
|
+
capability token is `Symbol.for('@dudousxd/nestjs-<lib>:<name>')`; because it goes
|
|
18
|
+
through the global symbol registry, producer and consumer resolve the SAME symbol.
|
|
19
|
+
It is the dependency-injection mirror of `channelName(lib, event)` for events.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @dudousxd/nestjs-diagnostics
|
|
25
|
+
# InjectCapability requires @nestjs/common
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A producer library binds its capability under the shared token:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Module } from '@nestjs/common';
|
|
32
|
+
import { capability } from '@dudousxd/nestjs-diagnostics';
|
|
33
|
+
import { ContextAccessorImpl } from './accessor.js';
|
|
34
|
+
|
|
35
|
+
// token === Symbol.for('@dudousxd/nestjs-context:accessor')
|
|
36
|
+
@Module({
|
|
37
|
+
providers: [
|
|
38
|
+
ContextAccessorImpl,
|
|
39
|
+
{ provide: capability('context', 'accessor'), useExisting: ContextAccessorImpl },
|
|
40
|
+
],
|
|
41
|
+
exports: [capability('context', 'accessor')],
|
|
42
|
+
})
|
|
43
|
+
export class ContextModule {}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Source: `packages/core/src/capability.ts`, `packages/core/src/inject-capability.ts`.
|
|
47
|
+
|
|
48
|
+
## Core patterns
|
|
49
|
+
|
|
50
|
+
### 1. capability(lib, name) — the shared token
|
|
51
|
+
|
|
52
|
+
`capability('context', 'accessor')` returns
|
|
53
|
+
`Symbol.for('@dudousxd/nestjs-context:accessor')`. Any code in any package that
|
|
54
|
+
calls it with the same `(lib, name)` gets the identical symbol, so a consumer can
|
|
55
|
+
inject a producer's capability with no static dependency between them.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { capability } from '@dudousxd/nestjs-diagnostics';
|
|
59
|
+
const TOKEN = capability('context', 'accessor');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Source: `packages/core/src/capability.ts` (`capability`).
|
|
63
|
+
|
|
64
|
+
### 2. InjectCapability(lib, name) — optional cross-lib injection
|
|
65
|
+
|
|
66
|
+
From the `/nestjs` subpath, `InjectCapability` is shorthand for
|
|
67
|
+
`@Optional() @Inject(capability(lib, name))` — when the producer library is
|
|
68
|
+
absent, the parameter is `undefined` instead of breaking injection:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { Injectable } from '@nestjs/common';
|
|
72
|
+
import { InjectCapability } from '@dudousxd/nestjs-diagnostics/nestjs';
|
|
73
|
+
import type { ContextAccessor } from '@dudousxd/nestjs-diagnostics';
|
|
74
|
+
|
|
75
|
+
@Injectable()
|
|
76
|
+
export class AuditService {
|
|
77
|
+
constructor(
|
|
78
|
+
@InjectCapability('context', 'accessor')
|
|
79
|
+
private readonly ctx?: ContextAccessor, // undefined when nestjs-context absent
|
|
80
|
+
) {}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You annotate the parameter type yourself (capability payload types are not auto-
|
|
85
|
+
inferred across repos).
|
|
86
|
+
|
|
87
|
+
Source: `packages/core/src/inject-capability.ts` (`InjectCapability`).
|
|
88
|
+
|
|
89
|
+
### 3. assertCapabilityNaming — anti-drift guard for contract tests
|
|
90
|
+
|
|
91
|
+
Assert every exported token of a library follows the canonical
|
|
92
|
+
`@dudousxd/nestjs-<lib>:<name>` naming (i.e. was minted by `capability(lib, …)`).
|
|
93
|
+
It throws naming the offending export, turning drift into a red test:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { assertCapabilityNaming, capability } from '@dudousxd/nestjs-diagnostics';
|
|
97
|
+
|
|
98
|
+
const tokens = { ACCESSOR: capability('context', 'accessor') };
|
|
99
|
+
|
|
100
|
+
it('all capability tokens follow the naming convention', () => {
|
|
101
|
+
expect(() => assertCapabilityNaming('context', tokens)).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Source: `packages/core/src/conformance.ts` (`assertCapabilityNaming`).
|
|
106
|
+
|
|
107
|
+
## Common mistakes
|
|
108
|
+
|
|
109
|
+
### Minting the token with Symbol() instead of capability()
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Wrong — a unique symbol; the consumer's capability('context','accessor') won't match it.
|
|
113
|
+
const TOKEN = Symbol('@dudousxd/nestjs-context:accessor');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// Correct — Symbol.for via capability() so all copies resolve the same token.
|
|
118
|
+
import { capability } from '@dudousxd/nestjs-diagnostics';
|
|
119
|
+
const TOKEN = capability('context', 'accessor');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`Symbol(desc)` is always unique; only `Symbol.for(key)` (what `capability` uses)
|
|
123
|
+
returns a process-shared symbol that producer and consumer both resolve.
|
|
124
|
+
Source: `packages/core/src/capability.ts` (`capability`).
|
|
125
|
+
|
|
126
|
+
### Injecting a capability without @Optional and crashing when the peer is absent
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// Wrong — hard @Inject throws "Nest can't resolve dependency" when the producer isn't loaded.
|
|
130
|
+
constructor(@Inject(capability('context', 'accessor')) private ctx: ContextAccessor) {}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// Correct — InjectCapability is already optional; ctx is undefined when absent.
|
|
135
|
+
constructor(@InjectCapability('context', 'accessor') private ctx?: ContextAccessor) {}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`InjectCapability` applies `Optional()` then `Inject(token)`, so a missing peer
|
|
139
|
+
yields `undefined` rather than a resolution error — the whole point of an optional
|
|
140
|
+
cross-library capability.
|
|
141
|
+
Source: `packages/core/src/inject-capability.ts` (`Optional()` + `Inject(token)`).
|
|
142
|
+
|
|
143
|
+
### Confusing capability tokens with event channels
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// Wrong — capability is a DI token, not a channel; you cannot subscribe to it.
|
|
147
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
148
|
+
diagnostics_channel.channel(capability('context', 'accessor') as any); // type/runtime error
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// Correct — events go over channelName(); capabilities are injected.
|
|
153
|
+
import { channelName } from '@dudousxd/nestjs-diagnostics';
|
|
154
|
+
diagnostics_channel.channel(channelName('billing', 'invoice-paid')).subscribe(fn);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Capabilities (`Symbol.for(...)` DI tokens) and channels (`aviary:<lib>:<event>`
|
|
158
|
+
strings) are the two distinct halves of the protocol — one is injected, the other
|
|
159
|
+
is published/subscribed.
|
|
160
|
+
Source: `packages/core/src/capability.ts`, `packages/core/src/channel.ts`.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: emit-diagnostics
|
|
3
|
+
description: Emit observability events from @dudousxd/nestjs-diagnostics with emit(lib, event, payload, opts) over node:diagnostics_channel on aviary:<lib>:<event>. Covers the DiagnosticEvent envelope (v, ts, lib, event, traceId, payload, durationMs), zero-overhead hasSubscribers gating, EmitOptions sampling and durationMs, typed ChannelRegistry declaration merging, and trace correlation via setContextAccessor / CONTEXT_ACCESSOR. No module to register; the core barrel is framework-agnostic.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
type: core
|
|
7
|
+
library: "@dudousxd/nestjs-diagnostics"
|
|
8
|
+
library_version: 0.6.0
|
|
9
|
+
framework: nestjs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Emit diagnostics events
|
|
13
|
+
|
|
14
|
+
`@dudousxd/nestjs-diagnostics` is a tiny convention for emitting observability
|
|
15
|
+
events over Node's built-in `node:diagnostics_channel`. A library calls
|
|
16
|
+
`emit('billing', 'invoice-paid', payload)` and is done — whoever wants to observe
|
|
17
|
+
subscribes. Emitting is effectively free when nothing is listening.
|
|
18
|
+
|
|
19
|
+
There is **nothing to register** to emit: no module, no provider. The main barrel
|
|
20
|
+
is a set of pure functions and is framework-agnostic (importable from non-Nest
|
|
21
|
+
code).
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add @dudousxd/nestjs-diagnostics
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Call `emit` from any provider where something interesting happens:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Injectable } from '@nestjs/common';
|
|
33
|
+
import { emit } from '@dudousxd/nestjs-diagnostics';
|
|
34
|
+
|
|
35
|
+
@Injectable()
|
|
36
|
+
export class BillingService {
|
|
37
|
+
async markInvoicePaid(invoiceId: string, amount: number) {
|
|
38
|
+
// … your domain logic …
|
|
39
|
+
emit('billing', 'invoice-paid', { invoiceId, amount });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every event flows over a channel named `aviary:<lib>:<event>` and carries a
|
|
45
|
+
standard `DiagnosticEvent` envelope:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
interface DiagnosticEvent<TPayload = unknown> {
|
|
49
|
+
v?: number; // SCHEMA_VERSION stamped by emit (currently 1)
|
|
50
|
+
ts: number; // Date.now() at publish time
|
|
51
|
+
lib: string; // the <lib>, e.g. "billing"
|
|
52
|
+
event: string; // the <event>, e.g. "invoice-paid"
|
|
53
|
+
traceId?: string; // auto-filled from a context accessor when present
|
|
54
|
+
payload: TPayload; // your library-defined data
|
|
55
|
+
durationMs?: number; // set only when EmitOptions.durationMs is passed
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Source: `packages/core/src/channel.ts`, `packages/core/src/types.ts`.
|
|
60
|
+
|
|
61
|
+
## Core patterns
|
|
62
|
+
|
|
63
|
+
### 1. Zero-overhead emit (the central guarantee)
|
|
64
|
+
|
|
65
|
+
`emit` builds and publishes the envelope **only when the channel has
|
|
66
|
+
subscribers** (`channel.hasSubscribers`), so a production process with no
|
|
67
|
+
observer attached pays essentially nothing per call — just two `Map.get`s and a
|
|
68
|
+
boolean read. It also **never throws**: observability must never break the
|
|
69
|
+
emitting code path.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// In prod with no Telescope/observer attached, this allocates no envelope.
|
|
73
|
+
emit('authz', 'decision', { subject, allow: true });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Source: `packages/core/src/channel.ts` (`emit`, the `hasSubscribers` gate).
|
|
77
|
+
|
|
78
|
+
### 2. EmitOptions: explicit traceId, sampling, durationMs
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { emit } from '@dudousxd/nestjs-diagnostics';
|
|
82
|
+
|
|
83
|
+
// Explicit trace id (wins over the registered accessor):
|
|
84
|
+
emit('billing', 'invoice-paid', payload, { traceId });
|
|
85
|
+
|
|
86
|
+
// Load-shedding on a hot event — shed 90%, consulted only AFTER hasSubscribers
|
|
87
|
+
// and BEFORE the envelope is built (no allocation when skipped):
|
|
88
|
+
emit('authz', 'decision', payload, { sample: () => Math.random() < 0.1 });
|
|
89
|
+
|
|
90
|
+
// Stamp a duration so downstream observers can build histograms:
|
|
91
|
+
emit('cache', 'lookup', { key }, { durationMs: 4.2 });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`sample` returning `false` (or throwing) sheds the event without allocating the
|
|
95
|
+
envelope. `durationMs` is set on the envelope only when provided, keeping the
|
|
96
|
+
common envelope shape monomorphic.
|
|
97
|
+
|
|
98
|
+
Source: `packages/core/src/types.ts` (`EmitOptions`), `packages/core/src/channel.ts`.
|
|
99
|
+
|
|
100
|
+
### 3. Compile-time payload types via the typed ChannelRegistry
|
|
101
|
+
|
|
102
|
+
By default every `(lib, event)` pair takes `payload: unknown`. A library can opt
|
|
103
|
+
specific channels into compile-time payload checking by augmenting
|
|
104
|
+
`ChannelRegistry` through TypeScript declaration merging — the untyped path stays
|
|
105
|
+
open for every other channel:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
declare module '@dudousxd/nestjs-diagnostics' {
|
|
109
|
+
interface ChannelRegistry {
|
|
110
|
+
billing: {
|
|
111
|
+
'invoice-paid': { invoiceId: string; amount: number };
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
emit('billing', 'invoice-paid', { invoiceId: 'inv_1', amount: 4200 }); // checked
|
|
117
|
+
emit('anything', 'else', whatever); // still unknown
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is a purely type-level mechanism: no runtime registry of payload shapes,
|
|
121
|
+
nothing allocated.
|
|
122
|
+
|
|
123
|
+
Source: `packages/core/src/types.ts` (`ChannelRegistry`, `LibOf`, `EventOf`, `PayloadOf`).
|
|
124
|
+
|
|
125
|
+
### 4. Auto-correlated traceId via setContextAccessor
|
|
126
|
+
|
|
127
|
+
If your app uses `@dudousxd/nestjs-context` (an optional peer — never imported by
|
|
128
|
+
this package), register its accessor once and every `emit` auto-fills `traceId`
|
|
129
|
+
from the current request:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { setContextAccessor, CONTEXT_ACCESSOR } from '@dudousxd/nestjs-diagnostics';
|
|
133
|
+
import { Inject, Optional, Injectable, type OnModuleInit } from '@nestjs/common';
|
|
134
|
+
import type { ContextAccessor } from '@dudousxd/nestjs-diagnostics';
|
|
135
|
+
|
|
136
|
+
@Injectable()
|
|
137
|
+
export class DiagnosticsContextBridge implements OnModuleInit {
|
|
138
|
+
constructor(
|
|
139
|
+
@Optional() @Inject(CONTEXT_ACCESSOR) private readonly accessor?: ContextAccessor,
|
|
140
|
+
) {}
|
|
141
|
+
|
|
142
|
+
onModuleInit() {
|
|
143
|
+
if (this.accessor) setContextAccessor(this.accessor);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`CONTEXT_ACCESSOR` is `Symbol.for('@dudousxd/nestjs-context:accessor')` — the same
|
|
149
|
+
token nestjs-context publishes under. Any object structurally matching the
|
|
150
|
+
`ContextAccessor` interface works. `resolveTraceId()` reads it, never throwing.
|
|
151
|
+
|
|
152
|
+
Source: `packages/core/src/context-accessor.ts`.
|
|
153
|
+
|
|
154
|
+
## Common mistakes
|
|
155
|
+
|
|
156
|
+
### Building the payload eagerly outside emit on a hot path
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// Wrong — serialize() runs every call even when nobody is listening.
|
|
160
|
+
const snapshot = serialize(hugeObject);
|
|
161
|
+
emit('jobs', 'tick', snapshot);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// Correct — gate expensive work behind hasSubscribers via getChannel.
|
|
166
|
+
import { getChannel, emit } from '@dudousxd/nestjs-diagnostics';
|
|
167
|
+
if (getChannel('jobs', 'tick').hasSubscribers) {
|
|
168
|
+
emit('jobs', 'tick', serialize(hugeObject));
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`emit` only skips allocating *its own* envelope when unsubscribed; it cannot skip
|
|
173
|
+
work you did before calling it. Read `hasSubscribers` yourself for expensive
|
|
174
|
+
payloads (or use `EmitOptions.sample` for cheap ones).
|
|
175
|
+
Source: `packages/core/src/channel.ts` (`emit`, `getChannel`).
|
|
176
|
+
|
|
177
|
+
### Expecting emit to throw or return a delivery result
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// Wrong — emit never throws and returns void; there is no ack to await/catch.
|
|
181
|
+
try {
|
|
182
|
+
const ok = await emit('billing', 'invoice-paid', payload);
|
|
183
|
+
} catch (e) { /* never reached */ }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// Correct — fire-and-forget; do not branch on its result.
|
|
188
|
+
emit('billing', 'invoice-paid', payload);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`emit` returns `void` and swallows every internal error so a broken observer can
|
|
192
|
+
never break the producer. There is no delivery guarantee, ordering, or replay.
|
|
193
|
+
Source: `packages/core/src/channel.ts` (`emit` `try/catch`).
|
|
194
|
+
|
|
195
|
+
### Putting the prefix or colons into lib/event yourself
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// Wrong — emit already prepends 'aviary:' and joins with ':'.
|
|
199
|
+
emit('aviary:billing', 'invoice:paid', payload); // channel: aviary:aviary:billing:invoice:paid
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// Correct — pass the bare lib and event segments.
|
|
204
|
+
emit('billing', 'invoice-paid', payload); // channel: aviary:billing:invoice-paid
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`channelName(lib, event)` builds `aviary:<lib>:<event>`; passing pre-joined names
|
|
208
|
+
produces a malformed channel that observers subscribed to `aviary:billing:*` will
|
|
209
|
+
miss.
|
|
210
|
+
Source: `packages/core/src/channel.ts` (`channelName`, `CHANNEL_PREFIX`).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observe-channels
|
|
3
|
+
description: Build a custom consumer of @dudousxd/nestjs-diagnostics events (OpenTelemetry span, APM, logger, test assertion) using the channel registry. Covers registeredChannels(), onChannelRegistered(cb), getChannel(lib, event), channelName(lib, event) and CHANNEL_PREFIX. Explains why node:diagnostics_channel has no wildcard, the subscribe-current-plus-future pattern, that subscribing flips hasSubscribers so producers start emitting, and resetRegistry for tests.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
type: core
|
|
7
|
+
library: "@dudousxd/nestjs-diagnostics"
|
|
8
|
+
library_version: 0.6.0
|
|
9
|
+
framework: nestjs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Observe channels with the registry
|
|
13
|
+
|
|
14
|
+
Anyone can consume `aviary:<lib>:<event>` events — Telescope is just one consumer,
|
|
15
|
+
and so is an OpenTelemetry span exporter, an APM bridge, a logger, or a test
|
|
16
|
+
assertion. This skill covers the **low-level registry** you use to build your own
|
|
17
|
+
generic consumer. (For NestJS provider methods, prefer `@OnDiagnostic`; for
|
|
18
|
+
Telescope, use the telescope extension.)
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @dudousxd/nestjs-diagnostics
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`node:diagnostics_channel` has **no wildcard subscription** — you can only
|
|
27
|
+
subscribe by exact channel name. So this package keeps a process-global registry
|
|
28
|
+
of every `aviary:<lib>:<event>` channel it has created. A generic consumer
|
|
29
|
+
subscribes to the current set and any registered later:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
33
|
+
import {
|
|
34
|
+
registeredChannels,
|
|
35
|
+
onChannelRegistered,
|
|
36
|
+
type DiagnosticEvent,
|
|
37
|
+
} from '@dudousxd/nestjs-diagnostics';
|
|
38
|
+
|
|
39
|
+
function record(name: string) {
|
|
40
|
+
diagnostics_channel.channel(name).subscribe((msg) => {
|
|
41
|
+
const e = msg as DiagnosticEvent;
|
|
42
|
+
console.log(`[${e.lib}:${e.event}]`, e.traceId, e.payload);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. subscribe to all current channels…
|
|
47
|
+
for (const name of registeredChannels()) record(name);
|
|
48
|
+
// 2. …and any registered in the future
|
|
49
|
+
const off = onChannelRegistered(record);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Source: `packages/core/src/registry.ts`, `packages/core/src/channel.ts` (`channelName`, `getChannel`).
|
|
53
|
+
|
|
54
|
+
## Core patterns
|
|
55
|
+
|
|
56
|
+
### 1. Current + future, never replay
|
|
57
|
+
|
|
58
|
+
`onChannelRegistered(cb)` fires once per channel registered **after** the call;
|
|
59
|
+
it does NOT replay existing channels. Always pair it with `registeredChannels()`
|
|
60
|
+
to cover both — exactly how the Telescope `DiagnosticWatcher` and the Redis relay
|
|
61
|
+
auto-subscribe. The returned function unsubscribes the listener.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const off = onChannelRegistered(record);
|
|
65
|
+
// later, on shutdown:
|
|
66
|
+
off();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Source: `packages/core/src/registry.ts` (`registeredChannels`, `onChannelRegistered`).
|
|
70
|
+
|
|
71
|
+
### 2. Subscribing turns producers on
|
|
72
|
+
|
|
73
|
+
A channel is only registered the first time `getChannel`/`emit`/`trace` touches a
|
|
74
|
+
`(lib, event)` pair. Subscribing to a channel also flips its `hasSubscribers` to
|
|
75
|
+
`true`, which is precisely what makes `emit()` start building and publishing
|
|
76
|
+
envelopes (zero overhead when nobody listens). So your consumer existing is what
|
|
77
|
+
makes producers pay the cost.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { getChannel } from '@dudousxd/nestjs-diagnostics';
|
|
81
|
+
// channelName(lib, event) === `aviary:${lib}:${event}` (CHANNEL_PREFIX === 'aviary')
|
|
82
|
+
const ch = getChannel('billing', 'invoice-paid'); // registers the name + memoizes the Channel
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Source: `packages/core/src/channel.ts` (`getChannel`, `emit` `hasSubscribers` gate),
|
|
86
|
+
`packages/core/src/registry.ts` (`registerChannel`).
|
|
87
|
+
|
|
88
|
+
### 3. Resetting the registry in tests
|
|
89
|
+
|
|
90
|
+
`resetRegistry()` is test-only: it forgets every registered channel and listener
|
|
91
|
+
(and clears the internal channel memo caches via reset hooks) so a suite can
|
|
92
|
+
assert registration in isolation. Do not call it in production code.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { resetRegistry, registeredChannels, emit } from '@dudousxd/nestjs-diagnostics';
|
|
96
|
+
|
|
97
|
+
beforeEach(() => resetRegistry());
|
|
98
|
+
|
|
99
|
+
it('registers the channel on first emit', () => {
|
|
100
|
+
emit('billing', 'invoice-paid', { invoiceId: 'x', amount: 1 });
|
|
101
|
+
expect(registeredChannels()).toContain('aviary:billing:invoice-paid');
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Source: `packages/core/src/registry.ts` (`resetRegistry`, `onRegistryReset`).
|
|
106
|
+
|
|
107
|
+
## Common mistakes
|
|
108
|
+
|
|
109
|
+
### Using onChannelRegistered alone and missing existing channels
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Wrong — channels emitted before this line are never subscribed.
|
|
113
|
+
onChannelRegistered(record);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// Correct — replay the current set, then listen for future ones.
|
|
118
|
+
for (const name of registeredChannels()) record(name);
|
|
119
|
+
const off = onChannelRegistered(record);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`onChannelRegistered` only fires for registrations that happen *after* it is
|
|
123
|
+
called; it deliberately does not replay history.
|
|
124
|
+
Source: `packages/core/src/registry.ts` (`onChannelRegistered` doc).
|
|
125
|
+
|
|
126
|
+
### Trying to subscribe with a wildcard channel name
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// Wrong — node:diagnostics_channel has no wildcard; this is a literal name.
|
|
130
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
131
|
+
diagnostics_channel.channel('aviary:billing:*').subscribe(record); // never fires
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// Correct — enumerate names from the registry and subscribe to each.
|
|
136
|
+
import { registeredChannels, onChannelRegistered } from '@dudousxd/nestjs-diagnostics';
|
|
137
|
+
for (const name of registeredChannels()) record(name);
|
|
138
|
+
const off = onChannelRegistered(record);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
There is no pattern subscription in `diagnostics_channel`; the registry exists
|
|
142
|
+
precisely to enumerate exact names for you.
|
|
143
|
+
Source: `packages/core/src/registry.ts` (module doc), `packages/core/src/channel.ts`.
|
|
144
|
+
|
|
145
|
+
### Throwing from a subscriber listener
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Wrong — publish() is synchronous; a throw lands in the producer's emit() call.
|
|
149
|
+
diagnostics_channel.channel(name).subscribe((msg) => {
|
|
150
|
+
JSON.parse((msg as any).payload.raw); // may throw → breaks the emitter
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// Correct — isolate your own consumer; never let it throw into emit().
|
|
156
|
+
diagnostics_channel.channel(name).subscribe((msg) => {
|
|
157
|
+
try { handle(msg as DiagnosticEvent); } catch (err) { logger.error(err); }
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The core swallows errors in `emit`/`registerChannel`, but a listener you attach
|
|
162
|
+
directly runs inside the synchronous `publish` — guard it yourself, as the
|
|
163
|
+
Telescope watcher's `safeRecord` does.
|
|
164
|
+
Source: `packages/telescope/src/diagnostic.watcher.ts` (`safeRecord`).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-with-on-diagnostic
|
|
3
|
+
description: React to diagnostics events inside a NestJS app with @OnDiagnostic(lib, event?) from @dudousxd/nestjs-diagnostics/nestjs, wired by DiagnosticsModule.forRoot(). Decorate provider methods to handle one exact aviary:<lib>:<event> channel or every aviary:<lib>:* channel (current and future). Handlers run on the DI-resolved instance; sync throws and async rejections are logged and swallowed so a reaction can never break the synchronous emit() that triggered it.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
type: core
|
|
7
|
+
library: "@dudousxd/nestjs-diagnostics"
|
|
8
|
+
library_version: 0.6.0
|
|
9
|
+
framework: nestjs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# React to events with @OnDiagnostic
|
|
13
|
+
|
|
14
|
+
The `@dudousxd/nestjs-diagnostics/nestjs` subpath adds an ergonomic way to react
|
|
15
|
+
to any `aviary:<lib>:<event>` event in a NestJS app — no extra event library.
|
|
16
|
+
Register the module once, then decorate provider methods.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @dudousxd/nestjs-diagnostics
|
|
22
|
+
# requires @nestjs/common, @nestjs/core, reflect-metadata (optional peers)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Register the module at the app root (it is `global`, imports `DiscoveryModule`):
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Module } from '@nestjs/common';
|
|
29
|
+
import { DiagnosticsModule } from '@dudousxd/nestjs-diagnostics/nestjs';
|
|
30
|
+
|
|
31
|
+
@Module({ imports: [DiagnosticsModule.forRoot()] })
|
|
32
|
+
export class AppModule {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then decorate provider methods:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Injectable } from '@nestjs/common';
|
|
39
|
+
import { OnDiagnostic } from '@dudousxd/nestjs-diagnostics/nestjs';
|
|
40
|
+
import type { DiagnosticEvent } from '@dudousxd/nestjs-diagnostics';
|
|
41
|
+
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class Reactions {
|
|
44
|
+
@OnDiagnostic('resilience', 'circuit-opened') // one exact channel
|
|
45
|
+
onCircuitOpen(e: DiagnosticEvent) { /* … */ }
|
|
46
|
+
|
|
47
|
+
@OnDiagnostic('resilience') // every aviary:resilience:* channel
|
|
48
|
+
onAnyResilience(e: DiagnosticEvent) { /* … */ }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Source: `packages/core/src/nestjs/diagnostics.module.ts`,
|
|
53
|
+
`packages/core/src/nestjs/on-diagnostic.decorator.ts`,
|
|
54
|
+
`packages/core/src/nestjs/diagnostics.explorer.ts`.
|
|
55
|
+
|
|
56
|
+
## Core patterns
|
|
57
|
+
|
|
58
|
+
### 1. Exact channel vs library wildcard
|
|
59
|
+
|
|
60
|
+
- `@OnDiagnostic('resilience', 'circuit-opened')` subscribes to the exact
|
|
61
|
+
`aviary:resilience:circuit-opened` channel.
|
|
62
|
+
- `@OnDiagnostic('resilience')` (no `event`) subscribes to **every**
|
|
63
|
+
`aviary:resilience:*` channel — both those already registered at bootstrap and
|
|
64
|
+
any that appear later (a library's first `emit('resilience', 'newevent', …)`).
|
|
65
|
+
|
|
66
|
+
The explorer scans providers on `onApplicationBootstrap`, then keeps an
|
|
67
|
+
`onChannelRegistered` callback alive so future wildcard channels are subscribed
|
|
68
|
+
automatically.
|
|
69
|
+
|
|
70
|
+
Source: `packages/core/src/nestjs/diagnostics.explorer.ts` (`onApplicationBootstrap`).
|
|
71
|
+
|
|
72
|
+
### 2. Handlers run on the DI-resolved instance
|
|
73
|
+
|
|
74
|
+
The decorated method is invoked on the real provider instance, so injected
|
|
75
|
+
dependencies are available:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
@Injectable()
|
|
79
|
+
export class AlertReactions {
|
|
80
|
+
constructor(private readonly notifier: Notifier) {}
|
|
81
|
+
|
|
82
|
+
@OnDiagnostic('billing', 'invoice-paid')
|
|
83
|
+
async onPaid(e: DiagnosticEvent<{ invoiceId: string; amount: number }>) {
|
|
84
|
+
await this.notifier.send(`Paid ${e.payload.invoiceId}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Source: `packages/core/src/nestjs/diagnostics.explorer.ts` (`safeInvoke` calls `fn.call(instance, event)`).
|
|
90
|
+
|
|
91
|
+
### 3. Never-throw isolation
|
|
92
|
+
|
|
93
|
+
`diagnostics_channel.publish` is **synchronous**, so a subscriber that throws
|
|
94
|
+
would propagate into the emitter's call stack. The explorer's `safeInvoke`
|
|
95
|
+
catches synchronous throws and attaches a `.catch` to rejected promises, logging
|
|
96
|
+
both — a reaction can never break the `emit()` that triggered it. Async handlers
|
|
97
|
+
are therefore fire-and-forget (publish cannot await).
|
|
98
|
+
|
|
99
|
+
Source: `packages/core/src/nestjs/diagnostics.explorer.ts` (`safeInvoke`, `logError`).
|
|
100
|
+
|
|
101
|
+
## Common mistakes
|
|
102
|
+
|
|
103
|
+
### Using @OnDiagnostic without importing DiagnosticsModule.forRoot()
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Wrong — decorator metadata is set, but nothing scans/subscribes it.
|
|
107
|
+
@Module({})
|
|
108
|
+
export class AppModule {}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Correct — forRoot() registers the explorer that wires every @OnDiagnostic.
|
|
113
|
+
@Module({ imports: [DiagnosticsModule.forRoot()] })
|
|
114
|
+
export class AppModule {}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`@OnDiagnostic` only records metadata; `DiagnosticsExplorer` (provided by
|
|
118
|
+
`forRoot()`) is what discovers decorated methods and subscribes the channels at
|
|
119
|
+
bootstrap.
|
|
120
|
+
Source: `packages/core/src/nestjs/diagnostics.module.ts` (`forRoot` provides `DiagnosticsExplorer`).
|
|
121
|
+
|
|
122
|
+
### Decorating a method on a non-provider class
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// Wrong — a plain class Nest never instantiates is never scanned.
|
|
126
|
+
class Reactions {
|
|
127
|
+
@OnDiagnostic('billing', 'invoice-paid') onPaid(e: DiagnosticEvent) {}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// Correct — @Injectable() and registered in a module's providers.
|
|
133
|
+
@Injectable()
|
|
134
|
+
export class Reactions {
|
|
135
|
+
@OnDiagnostic('billing', 'invoice-paid') onPaid(e: DiagnosticEvent) {}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The explorer iterates `discovery.getProviders()`; only DI-managed provider
|
|
140
|
+
instances are scanned for `@OnDiagnostic` metadata.
|
|
141
|
+
Source: `packages/core/src/nestjs/diagnostics.explorer.ts` (`discovery.getProviders()`).
|
|
142
|
+
|
|
143
|
+
### Throwing in a handler expecting the framework to retry
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// Wrong — assuming a throw bubbles up or triggers a retry.
|
|
147
|
+
@OnDiagnostic('billing', 'invoice-paid')
|
|
148
|
+
onPaid(e: DiagnosticEvent) {
|
|
149
|
+
throw new Error('will be retried'); // it will NOT — it is logged and swallowed
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Correct — handle failures inside the reaction; it is fire-and-forget.
|
|
155
|
+
@OnDiagnostic('billing', 'invoice-paid')
|
|
156
|
+
async onPaid(e: DiagnosticEvent) {
|
|
157
|
+
try { await this.work(e); } catch (err) { this.logger.warn(err); }
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`safeInvoke` swallows throws/rejections by design, so emission stays safe — there
|
|
162
|
+
is no delivery guarantee, retry, or replay.
|
|
163
|
+
Source: `packages/core/src/nestjs/diagnostics.explorer.ts` (`safeInvoke`).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trace-spans
|
|
3
|
+
description: Emit span-like start/end/asyncStart/asyncEnd/error events with trace(lib, event, fn, payload, opts) and tracingChannel(lib, event) from @dudousxd/nestjs-diagnostics. Wraps sync or async operations over aviary:<lib>:<event>:<phase> sub-channels, producing SpanEvent envelopes with spanId, durationMs, result and error so observers reconstruct real spans. Zero overhead when no span sub-channel has subscribers; never throws; SPAN_SCHEMA_VERSION, traceChannelNames.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
type: core
|
|
7
|
+
library: "@dudousxd/nestjs-diagnostics"
|
|
8
|
+
library_version: 0.6.0
|
|
9
|
+
framework: nestjs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Trace operations as spans
|
|
13
|
+
|
|
14
|
+
`emit` publishes a single POINT event. `trace` wraps an operation and publishes
|
|
15
|
+
span-like **start / end / asyncStart / asyncEnd / error** events with timing, so
|
|
16
|
+
observers can reconstruct real start↔end↔error pairs (durable steps, authz
|
|
17
|
+
decisions, cache fetches). It mirrors Node's `diagnostics_channel.tracingChannel`
|
|
18
|
+
sub-channels but stays inside the `aviary:` convention.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @dudousxd/nestjs-diagnostics
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Injectable } from '@nestjs/common';
|
|
28
|
+
import { trace } from '@dudousxd/nestjs-diagnostics';
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class AuthzService {
|
|
32
|
+
evaluate(req: Request) {
|
|
33
|
+
// Sync op: publishes start then end (with result) — or error on throw.
|
|
34
|
+
return trace('authz', 'decision', () => this.run(req), { subject: req.user });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async runStep(input: StepInput) {
|
|
38
|
+
// Async op: start, end (sync portion), asyncStart, then asyncEnd (with result).
|
|
39
|
+
return await trace('durable', 'step', () => this.execute(input), { name: 'charge' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Span events ride `aviary:<lib>:<event>:<phase>`; the base `aviary:<lib>:<event>`
|
|
45
|
+
name is what gets registered for discovery. A `SpanEvent` carries `v`, `ts`,
|
|
46
|
+
`lib`, `event`, `phase`, a per-span `spanId`, optional `traceId`, and
|
|
47
|
+
`payload` / `result` / `error` / `durationMs` depending on the phase.
|
|
48
|
+
|
|
49
|
+
Source: `packages/core/src/trace.ts`, `packages/core/src/types.ts` (`SpanEvent`, `SpanPhase`).
|
|
50
|
+
|
|
51
|
+
## Core patterns
|
|
52
|
+
|
|
53
|
+
### 1. The span lifecycle
|
|
54
|
+
|
|
55
|
+
- **Sync `fn`** → `start` (carries `payload`), then on return `end` (carries
|
|
56
|
+
`result` + `durationMs`); on throw `error` (carries `error` + `durationMs`).
|
|
57
|
+
The return value / throw is propagated to the caller unchanged.
|
|
58
|
+
- **Async `fn`** (returns a promise) → `start` synchronously, then `end` for the
|
|
59
|
+
sync portion, `asyncStart`, and finally `asyncEnd` (with `result` + `durationMs`)
|
|
60
|
+
on fulfilment or `error` on rejection. The promise is returned to the caller.
|
|
61
|
+
|
|
62
|
+
Every phase event of one `trace` call shares a `spanId`, so observers pair them
|
|
63
|
+
without relying on subscription order. `durationMs` is measured with
|
|
64
|
+
`performance.now()`.
|
|
65
|
+
|
|
66
|
+
Source: `packages/core/src/trace.ts` (`trace`, `publishPhase`).
|
|
67
|
+
|
|
68
|
+
### 2. Zero overhead when unobserved
|
|
69
|
+
|
|
70
|
+
When NO span sub-channel has a subscriber, `trace` calls `fn()` and returns its
|
|
71
|
+
value directly — no `spanId`, no envelope, no timing. The hot path is just five
|
|
72
|
+
`hasSubscribers` reads (`anySubscribed`).
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// In prod with no observer: equivalent to `return this.run(req)`.
|
|
76
|
+
const decision = trace('authz', 'decision', () => this.run(req), { subject });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Source: `packages/core/src/trace.ts` (`anySubscribed`, the early `return fn()`).
|
|
80
|
+
|
|
81
|
+
### 3. tracingChannel for a hot, fixed call site
|
|
82
|
+
|
|
83
|
+
Bind `trace` to one `(lib, event)` pair when a call site always traces the same
|
|
84
|
+
operation:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { tracingChannel } from '@dudousxd/nestjs-diagnostics';
|
|
88
|
+
|
|
89
|
+
const decision = tracingChannel('authz', 'decision');
|
|
90
|
+
// decision.name -> 'aviary:authz:decision'
|
|
91
|
+
// decision.channels -> the five span sub-channel names (traceChannelNames)
|
|
92
|
+
|
|
93
|
+
const out = decision.trace(() => evaluate(req), { subject });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Source: `packages/core/src/trace.ts` (`tracingChannel`, `TracingChannel`, `traceChannelNames`).
|
|
97
|
+
|
|
98
|
+
## Common mistakes
|
|
99
|
+
|
|
100
|
+
### Passing an already-invoked promise instead of a thunk
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// Wrong — fn() is called by you, so trace can't bracket start/end around it.
|
|
104
|
+
trace('durable', 'step', this.execute(input)); // type error: not a function
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// Correct — pass a zero-arg function; trace invokes it between start and end.
|
|
109
|
+
await trace('durable', 'step', () => this.execute(input), { name: 'charge' });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`trace(lib, event, fn, payload?, opts?)` takes a `fn: () => R` it calls itself;
|
|
113
|
+
the promise must be created inside the thunk so `start` precedes it.
|
|
114
|
+
Source: `packages/core/src/trace.ts` (`trace` signature).
|
|
115
|
+
|
|
116
|
+
### Forgetting to return / await the trace result
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Wrong — the operation runs but the caller loses its value and unhandled rejections.
|
|
120
|
+
async charge() {
|
|
121
|
+
trace('durable', 'step', () => this.execute());
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Correct — trace returns fn's value (or its promise); return/await it.
|
|
127
|
+
async charge() {
|
|
128
|
+
return await trace('durable', 'step', () => this.execute());
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`trace` is transparent: it returns exactly what `fn` returns (the same promise for
|
|
133
|
+
async). Drop the return and you drop the result and its error handling.
|
|
134
|
+
Source: `packages/core/src/trace.ts` (`trace` returns `result` / the settled promise).
|
|
135
|
+
|
|
136
|
+
### Subscribing only to the base channel and expecting span events
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// Wrong — span phases are published on sub-channels, not the base name.
|
|
140
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
141
|
+
diagnostics_channel.channel('aviary:authz:decision').subscribe(onSpan); // never fires
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// Correct — subscribe to the phase sub-channels via traceChannelNames.
|
|
146
|
+
import { traceChannelNames } from '@dudousxd/nestjs-diagnostics';
|
|
147
|
+
const n = traceChannelNames('authz', 'decision');
|
|
148
|
+
for (const name of [n.start, n.end, n.asyncStart, n.asyncEnd, n.error]) {
|
|
149
|
+
diagnostics_channel.channel(name).subscribe(onSpan);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`trace` publishes on `aviary:<lib>:<event>:start|end|asyncStart|asyncEnd|error`;
|
|
154
|
+
only the base name is registered for discovery, so a generic observer must derive
|
|
155
|
+
the sub-channels.
|
|
156
|
+
Source: `packages/core/src/trace.ts` (`traceChannelNames`, `getSpanChannels`).
|