@dunx/create-app 3.1.3 → 3.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/README.md +21 -16
- package/dist/{chunk-tn4dnvcw.js → chunk-rv2w0t23.js} +25 -13
- package/dist/cli.js +1 -1
- package/dist/generate.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/features/chat/chat.gateway.ts +11 -1
- package/templates/features/database/database.module.ts +20 -13
- package/templates/features/http/http-options.ts +2 -3
- package/templates/features/http/trace.controller.ts +7 -3
- package/templates/features/schedule/maintenance.service.ts +12 -12
- package/templates/features/stats/stats.demo.ts +69 -0
- package/templates/features/stats/stats.module.ts +14 -0
- package/templates/features/upstream/upstream.demo.ts +1 -1
- package/templates/features/upstream/upstream.module.ts +4 -2
- package/templates/minimal/README.md +4 -4
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ There is no flag for choosing features. The command opens a list:
|
|
|
40
40
|
| Ctrl+C, Esc | Stop, having written nothing |
|
|
41
41
|
|
|
42
42
|
`◉` is chosen, `◈` is pulled in by something else you chose, `○` is neither. The
|
|
43
|
-
two lines under the list update as you go: what your selection drags in,
|
|
44
|
-
of it needs Redis or Postgres running to do anything.
|
|
43
|
+
two lines under the list update as you go: one shows what your selection drags in,
|
|
44
|
+
the other shows which of it needs Redis or Postgres running to do anything.
|
|
45
45
|
|
|
46
46
|
Three more questions appear only when there is something to ask: a directory, when
|
|
47
47
|
the command line named none; a package name, when the directory's is one npm would
|
|
@@ -56,9 +56,10 @@ reject; and whether to write into a directory that already has files in it.
|
|
|
56
56
|
| `--yes`, `-y` | off | Skip the questions, take the minimal template |
|
|
57
57
|
| `--help` | | Print usage |
|
|
58
58
|
|
|
59
|
-
**Piped, redirected or in CI it asks nothing
|
|
60
|
-
a script never hangs on a question nothing can answer. To choose
|
|
61
|
-
a terminal, call [`scaffold`](#programmatic-use) rather than
|
|
59
|
+
**Piped, redirected or in CI it asks nothing.** It writes the minimal template
|
|
60
|
+
instead, so a script never hangs on a question nothing can answer. To choose
|
|
61
|
+
features without a terminal, call [`scaffold`](#programmatic-use) rather than
|
|
62
|
+
passing flags.
|
|
62
63
|
|
|
63
64
|
The name is validated against npm's rules **before** anything is created, because
|
|
64
65
|
an invalid one would otherwise surface as a confusing `bun install` failure inside
|
|
@@ -73,8 +74,8 @@ bunx @dunx/create-app .
|
|
|
73
74
|
|
|
74
75
|
`.git`, `.gitkeep`, `.DS_Store` and `LICENSE` do not count as contents, so a fresh
|
|
75
76
|
repo or a clone of an empty GitHub repository is a valid target without a question.
|
|
76
|
-
Nothing else is ignored: `.gitignore` and `README.md` both come out of the template
|
|
77
|
-
|
|
77
|
+
Nothing else is ignored: `.gitignore` and `README.md` both come out of the template.
|
|
78
|
+
Overwriting your copy of either is what the last question asks about.
|
|
78
79
|
|
|
79
80
|
## What a composed app looks like
|
|
80
81
|
|
|
@@ -89,9 +90,10 @@ my-api/
|
|
|
89
90
|
bunfig.toml the transform preload
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
Three files are generated for the selection and the feature directories are
|
|
93
|
-
`main.ts` is one file rather than two: a test imports `createApp` from it,
|
|
94
|
-
`import.meta.main` block at the bottom
|
|
93
|
+
Three files are generated for the selection, and the feature directories are just
|
|
94
|
+
copied. `main.ts` is one file rather than two: a test imports `createApp` from it,
|
|
95
|
+
and the `import.meta.main` block at the bottom stops that same import from
|
|
96
|
+
starting a server.
|
|
95
97
|
|
|
96
98
|
**There is no worker entry point, even with queues.** `QueueModule` is given
|
|
97
99
|
`consume: true`, so the container opens the bullmq workers at `onInit` and closes
|
|
@@ -100,18 +102,21 @@ forked by bullmq itself into `src/jobs/jobs.processor.ts`.
|
|
|
100
102
|
|
|
101
103
|
## What it generates
|
|
102
104
|
|
|
103
|
-
The `minimal` template
|
|
104
|
-
[`examples/minimal`](https://github.com/petarzarkov/dunx/tree/main/examples/minimal)
|
|
105
|
+
The `minimal` template is the same app as
|
|
106
|
+
[`examples/minimal`](https://github.com/petarzarkov/dunx/tree/main/examples/minimal):
|
|
107
|
+
a service, a controller, a module, `HttpFactory.create`, one test against a real
|
|
105
108
|
server, and the `bunfig.toml` preload line that makes constructor injection work.
|
|
106
109
|
|
|
107
|
-
Its `src/` is a **byte-for-byte copy** of that example
|
|
110
|
+
Its `src/` is a **byte-for-byte copy** of that example. A test in this package
|
|
108
111
|
fails if the two ever drift.
|
|
109
112
|
|
|
110
113
|
Every app also gets an `AGENTS.md` naming its layout, its commands and the rules
|
|
111
114
|
dunx fails at boot over, plus a `CLAUDE.md` pointing at it. Both link
|
|
112
115
|
<https://petarzarkov.github.io/dunx/setup.md>, which is served per release, rather
|
|
113
|
-
than copying the framework's own instructions into your repository.
|
|
114
|
-
|
|
116
|
+
than copying the framework's own instructions into your repository.
|
|
117
|
+
|
|
118
|
+
The example is the one CI boots, so keeping them identical makes the template
|
|
119
|
+
trustworthy rather than merely plausible.
|
|
115
120
|
|
|
116
121
|
## Two details
|
|
117
122
|
|
|
@@ -146,7 +151,7 @@ const { directory, files } = await scaffold({
|
|
|
146
151
|
```
|
|
147
152
|
|
|
148
153
|
This is the scripted path the removed `--with` flag used to be. `features` takes
|
|
149
|
-
the same names the list shows, in any order, and pulls in what they require
|
|
154
|
+
the same names the list shows, in any order, and pulls in what they require.
|
|
150
155
|
`FEATURES` exports the set. Omitting it writes the minimal template.
|
|
151
156
|
|
|
152
157
|
`scaffold` throws `ScaffoldError` for anything the caller can fix - an unknown
|
|
@@ -284,6 +284,15 @@ var FEATURES = [
|
|
|
284
284
|
dependencies: [],
|
|
285
285
|
config: []
|
|
286
286
|
},
|
|
287
|
+
{
|
|
288
|
+
name: "stats",
|
|
289
|
+
source: "stats",
|
|
290
|
+
summary: "Per-route request timings and per-operation query timings, as JSON.",
|
|
291
|
+
requires: ["database"],
|
|
292
|
+
module: { klass: "StatsModule", from: "./stats/stats.module.js" },
|
|
293
|
+
dependencies: [],
|
|
294
|
+
config: []
|
|
295
|
+
},
|
|
287
296
|
{
|
|
288
297
|
name: "client",
|
|
289
298
|
source: "upstream",
|
|
@@ -495,11 +504,11 @@ var manifest = (features) => {
|
|
|
495
504
|
`;
|
|
496
505
|
};
|
|
497
506
|
var THIRD_PARTY = Object.freeze({
|
|
498
|
-
zod: "
|
|
499
|
-
"drizzle-orm": "
|
|
500
|
-
"better-auth": "
|
|
501
|
-
bullmq: "
|
|
502
|
-
ioredis: "
|
|
507
|
+
zod: "4.5.4",
|
|
508
|
+
"drizzle-orm": "0.45.2",
|
|
509
|
+
"better-auth": "1.6.25",
|
|
510
|
+
bullmq: "6.0.5",
|
|
511
|
+
ioredis: "6.0.0"
|
|
503
512
|
});
|
|
504
513
|
var versionOf = (dep) => THIRD_PARTY[dep] ?? "latest";
|
|
505
514
|
var appModule = (name, features) => {
|
|
@@ -642,14 +651,17 @@ var main = (name, features) => {
|
|
|
642
651
|
version: '0.1.0',
|
|
643
652
|
root: AppModule,
|
|
644
653
|
})` : "AppModule";
|
|
645
|
-
const options =
|
|
646
|
-
"
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
654
|
+
const options = [
|
|
655
|
+
...has(features, "stats") ? ["metrics: true,"] : [],
|
|
656
|
+
...websockets ? [
|
|
657
|
+
"// Multi-node websocket fan-out on `Bun.RedisClient`, so it costs no",
|
|
658
|
+
"// dependency. With no Redis running this degrades to single-process",
|
|
659
|
+
"// behaviour, logs one warning, and the app still boots.",
|
|
660
|
+
"websocket: { idleTimeout: 30 },",
|
|
661
|
+
"relay: new RedisRelay({ connectionTimeout: 500 }),",
|
|
662
|
+
"relayChannel: RELAY_CHANNEL,"
|
|
663
|
+
] : []
|
|
664
|
+
];
|
|
653
665
|
const shaping = [
|
|
654
666
|
"app.setGlobalPrefix('api');",
|
|
655
667
|
...assets ? ["app.use(StaticFiles);"] : [],
|
package/dist/cli.js
CHANGED
package/dist/generate.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { type Feature } from './features.js';
|
|
|
3
3
|
export declare const configGroupsFor: (features: readonly Feature[]) => readonly string[];
|
|
4
4
|
export declare const manifest: (features: readonly Feature[]) => string;
|
|
5
5
|
/**
|
|
6
|
-
* Third-party
|
|
6
|
+
* Third-party versions, exact for the reason dunx's own manifests are, and
|
|
7
|
+
* written here rather than read off `examples/full` at run time:
|
|
7
8
|
* the generated app installs from npm and the example installs from the workspace,
|
|
8
9
|
* so the example's manifest is not a statement about what a consumer should take.
|
|
9
10
|
* `features.test.ts` checks these against the example's, which is what stops them
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger } from '@dunx/core';
|
|
1
|
+
import { Gauge, Logger } from '@dunx/core';
|
|
2
2
|
import {
|
|
3
3
|
Gateway,
|
|
4
4
|
HttpStatusCode,
|
|
@@ -18,6 +18,9 @@ import { Lobby } from './lobby.service.js';
|
|
|
18
18
|
* upgrade as a native route. */
|
|
19
19
|
@Gateway('/chat')
|
|
20
20
|
export class ChatGateway {
|
|
21
|
+
/** A `Gauge` rather than a `Counter`: connections close as well as open. */
|
|
22
|
+
readonly #live = new Gauge();
|
|
23
|
+
|
|
21
24
|
constructor(
|
|
22
25
|
private readonly lobby: Lobby,
|
|
23
26
|
private readonly logger: Logger,
|
|
@@ -41,9 +44,15 @@ export class ChatGateway {
|
|
|
41
44
|
opened(socket: Socket): void {
|
|
42
45
|
// Bun's own pub/sub: topics live in the runtime.
|
|
43
46
|
socket.subscribe(Lobby.TOPIC);
|
|
47
|
+
this.#live.inc();
|
|
44
48
|
socket.send('welcome');
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/** What the gauge reads, for the tour and for anything else that asks. */
|
|
52
|
+
get live(): number {
|
|
53
|
+
return this.#live.value;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
@OnMessage('say')
|
|
48
57
|
say(text: string): { delivered: number } {
|
|
49
58
|
// Broadcast reaches every subscriber; the return value replies to the sender.
|
|
@@ -69,6 +78,7 @@ export class ChatGateway {
|
|
|
69
78
|
|
|
70
79
|
@OnClose()
|
|
71
80
|
closed(socket: Socket, code: number): void {
|
|
81
|
+
this.#live.dec();
|
|
72
82
|
this.logger.info(`${socket.data.path} closed with ${code}`);
|
|
73
83
|
}
|
|
74
84
|
}
|
|
@@ -2,6 +2,7 @@ import { Module } from '@dunx/core';
|
|
|
2
2
|
import {
|
|
3
3
|
DbConnection,
|
|
4
4
|
DbModule,
|
|
5
|
+
QueryMetrics,
|
|
5
6
|
SyncDatabase,
|
|
6
7
|
SyncSqliteOptions,
|
|
7
8
|
} from '@dunx/infra/db';
|
|
@@ -18,18 +19,24 @@ import * as schema from './schema.js';
|
|
|
18
19
|
// `SyncSqliteOptions` runs SQLite in synchronous mode, so the token is
|
|
19
20
|
// `SyncDatabase` and `transactionSync` is reachable. `SqliteOptions` is the
|
|
20
21
|
// default and what an app wants if it might move to Postgres.
|
|
21
|
-
DbModule.forRootAsync(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
DbModule.forRootAsync(
|
|
23
|
+
SyncDatabase,
|
|
24
|
+
{
|
|
25
|
+
useFactory: (config: AppConfigService) =>
|
|
26
|
+
new SyncSqliteOptions({
|
|
27
|
+
// Required: the type argument every constructor below sees.
|
|
28
|
+
schema,
|
|
29
|
+
// A dotted path, checked against AppConfig the same way a top-level
|
|
30
|
+
// key is. `config.get('database').file` still reads the same value.
|
|
31
|
+
filename: config.get('database.file'),
|
|
32
|
+
pragmas: ['foreign_keys = ON'],
|
|
33
|
+
}),
|
|
34
|
+
inject: [AppConfigService],
|
|
35
|
+
},
|
|
36
|
+
// Times every query the driver runs, readable through `QueryMetrics` and
|
|
37
|
+
// shown on the dashboard's stats panel. Off by default.
|
|
38
|
+
{ metrics: true },
|
|
39
|
+
),
|
|
33
40
|
],
|
|
34
41
|
controllers: [LedgerController],
|
|
35
42
|
providers: [Ledger],
|
|
@@ -37,6 +44,6 @@ import * as schema from './schema.js';
|
|
|
37
44
|
* Re-exported so importers can inject it. `DbModule` exports to this module
|
|
38
45
|
* only; naming it again passes it on.
|
|
39
46
|
*/
|
|
40
|
-
exports: [SyncDatabase, DbConnection, Ledger],
|
|
47
|
+
exports: [SyncDatabase, DbConnection, Ledger, QueryMetrics],
|
|
41
48
|
})
|
|
42
49
|
export class DatabaseModule {}
|
|
@@ -66,9 +66,8 @@ export class AppHttpOptions extends HttpOptionsProvider {
|
|
|
66
66
|
responseBody: log.responseBody,
|
|
67
67
|
// The dashboard polls every five seconds and would bury everything else.
|
|
68
68
|
ignorePrefix: ['/api/_dunx'],
|
|
69
|
-
//
|
|
70
|
-
// `
|
|
71
|
-
trace: true,
|
|
69
|
+
// `trace` and `traceResponse` are not set: both are on by default, and
|
|
70
|
+
// `TraceController` reading the trace back proves it.
|
|
72
71
|
};
|
|
73
72
|
}
|
|
74
73
|
}
|
|
@@ -5,8 +5,9 @@ import { Controller, Get, type Input, type RouteSchemas } from '@dunx/http';
|
|
|
5
5
|
* What the request's W3C trace looks like from inside a handler.
|
|
6
6
|
*
|
|
7
7
|
* `RequestContext` is bound by `@dunx/core` whatever else the app imports, and
|
|
8
|
-
*
|
|
9
|
-
* log line this request writes carries the same
|
|
8
|
+
* request logging puts the trace fields into it with nothing configured. Every
|
|
9
|
+
* log line this request writes carries the same values, and the response carries
|
|
10
|
+
* `traceresponse`. `requestLogging: { trace: false }` is what removes them.
|
|
10
11
|
*/
|
|
11
12
|
@Controller('trace')
|
|
12
13
|
export class TraceController {
|
|
@@ -17,13 +18,16 @@ export class TraceController {
|
|
|
17
18
|
traceId: string | undefined;
|
|
18
19
|
spanId: string | undefined;
|
|
19
20
|
parentSpanId: string | undefined;
|
|
21
|
+
traceFlags: string | undefined;
|
|
20
22
|
inbound: string | null;
|
|
21
23
|
} {
|
|
22
|
-
const { traceId, spanId, parentSpanId } =
|
|
24
|
+
const { traceId, spanId, parentSpanId, traceFlags } =
|
|
25
|
+
this.context.getContext();
|
|
23
26
|
return {
|
|
24
27
|
traceId: traceId as string | undefined,
|
|
25
28
|
spanId: spanId as string | undefined,
|
|
26
29
|
parentSpanId: parentSpanId as string | undefined,
|
|
30
|
+
traceFlags: traceFlags as string | undefined,
|
|
27
31
|
inbound: input.req.headers.get('traceparent'),
|
|
28
32
|
};
|
|
29
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger } from '@dunx/core';
|
|
1
|
+
import { Counter, Logger } from '@dunx/core';
|
|
2
2
|
import { Cron, Interval, OnceOnBoot } from '@dunx/infra/schedule';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -6,13 +6,13 @@ import { Cron, Interval, OnceOnBoot } from '@dunx/infra/schedule';
|
|
|
6
6
|
* with no second registration. Nothing here coordinates across replicas: work
|
|
7
7
|
* that must happen once per fleet is a `@JobHandler`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* a class with both a decorated member
|
|
11
|
-
* field. See docs/bun-apis.md.
|
|
9
|
+
* Counts go through `@dunx/core`'s `Counter` rather than a private field, which
|
|
10
|
+
* also sidesteps Bun 1.4.0 failing to parse a class with both a decorated member
|
|
11
|
+
* and a read-modify-write on a private field. See docs/bun-apis.md.
|
|
12
12
|
*/
|
|
13
13
|
export class Maintenance {
|
|
14
|
-
#sweeps =
|
|
15
|
-
#compactions =
|
|
14
|
+
readonly #sweeps = new Counter();
|
|
15
|
+
readonly #compactions = new Counter();
|
|
16
16
|
#warmed = false;
|
|
17
17
|
|
|
18
18
|
constructor(private readonly logger: Logger) {}
|
|
@@ -28,8 +28,8 @@ export class Maintenance {
|
|
|
28
28
|
/** Ten minutes, so it never fires during a tour. `trigger` runs it now. */
|
|
29
29
|
@Interval(600_000, { name: 'maintenance.sweep' })
|
|
30
30
|
sweepSessions(): number {
|
|
31
|
-
this.#sweeps
|
|
32
|
-
return this.#sweeps;
|
|
31
|
+
this.#sweeps.inc();
|
|
32
|
+
return this.#sweeps.value;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -39,14 +39,14 @@ export class Maintenance {
|
|
|
39
39
|
@Cron('0 3 * * *', { name: 'maintenance.compact' })
|
|
40
40
|
async compactLedger(): Promise<number> {
|
|
41
41
|
await Bun.sleep(1);
|
|
42
|
-
this.#compactions
|
|
43
|
-
return this.#compactions;
|
|
42
|
+
this.#compactions.inc();
|
|
43
|
+
return this.#compactions.value;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
get counts(): { sweeps: number; compactions: number; warmed: boolean } {
|
|
47
47
|
return {
|
|
48
|
-
sweeps: this.#sweeps,
|
|
49
|
-
compactions: this.#compactions,
|
|
48
|
+
sweeps: this.#sweeps.value,
|
|
49
|
+
compactions: this.#compactions.value,
|
|
50
50
|
warmed: this.#warmed,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventLoopLag, Logger } from '@dunx/core';
|
|
2
|
+
import { RequestMetrics } from '@dunx/http';
|
|
3
|
+
import { QueryMetrics } from '@dunx/infra/db';
|
|
4
|
+
|
|
5
|
+
/** Read out of the same container, from work the tour already did. */
|
|
6
|
+
export class StatsDemo {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly logger: Logger,
|
|
9
|
+
private readonly requests: RequestMetrics,
|
|
10
|
+
private readonly queries: QueryMetrics,
|
|
11
|
+
private readonly lag: EventLoopLag,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
demonstrate(): void {
|
|
15
|
+
const http = this.requests.snapshot();
|
|
16
|
+
const ms = (nanoseconds: number | undefined): string =>
|
|
17
|
+
nanoseconds === undefined ? '-' : `${(nanoseconds / 1e6).toFixed(2)}ms`;
|
|
18
|
+
|
|
19
|
+
this.logger.info(
|
|
20
|
+
`${http.routes.length} route series, ${http.inFlight} in flight, ` +
|
|
21
|
+
`${http.pendingWebSockets} sockets - both read off Bun.serve, not counted`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const slowest = [...http.routes]
|
|
25
|
+
.sort((a, b) => (b.duration.p99 ?? 0) - (a.duration.p99 ?? 0))
|
|
26
|
+
.slice(0, 3);
|
|
27
|
+
for (const route of slowest) {
|
|
28
|
+
this.logger.info(
|
|
29
|
+
`${route.method} ${route.route}: ${route.count} calls, ` +
|
|
30
|
+
`p50 ${ms(route.duration.p50)} p99 ${ms(route.duration.p99)} ` +
|
|
31
|
+
`max ${ms(route.duration.max)}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const worst = slowest[0];
|
|
36
|
+
if (worst?.slowestTraceId !== undefined) {
|
|
37
|
+
this.logger.info(
|
|
38
|
+
`the slowest ${worst.route} call has traceId ${worst.slowestTraceId} - ` +
|
|
39
|
+
'every line that request wrote carries it',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const unmatched = http.routes.find(
|
|
44
|
+
(route) => route.route === '(unmatched)',
|
|
45
|
+
);
|
|
46
|
+
this.logger.info(
|
|
47
|
+
`unmatched paths: ${unmatched?.count ?? 0} across one series, so a ` +
|
|
48
|
+
'scanner walking urls cannot grow the series count',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const db = this.queries.snapshot();
|
|
52
|
+
this.logger.info(
|
|
53
|
+
`${db.total} queries, timed at the bun:sqlite handle dunx constructs - ` +
|
|
54
|
+
"drizzle's own logQuery fires before the statement runs and cannot time one",
|
|
55
|
+
);
|
|
56
|
+
for (const operation of db.operations) {
|
|
57
|
+
this.logger.info(
|
|
58
|
+
`${operation.operation}: ${operation.count} calls, ` +
|
|
59
|
+
`${operation.errors} failed, p99 ${ms(operation.duration.p99)}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const loop = this.lag.snapshot();
|
|
64
|
+
this.logger.info(
|
|
65
|
+
`event loop lag: ${loop.count} samples, p99 ${ms(loop.p99)} ` +
|
|
66
|
+
`max ${ms(loop.max)}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EventLoopLag, Module } from '@dunx/core';
|
|
2
|
+
import { DatabaseModule } from '../database/database.module.js';
|
|
3
|
+
import { StatsDemo } from './stats.demo.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `RequestMetrics` is global, so only `QueryMetrics` is imported. `EventLoopLag`
|
|
7
|
+
* is a provider so its `onInit` enables it at boot rather than at read time.
|
|
8
|
+
*/
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [DatabaseModule],
|
|
11
|
+
providers: [EventLoopLag, StatsDemo],
|
|
12
|
+
exports: [StatsDemo],
|
|
13
|
+
})
|
|
14
|
+
export class StatsModule {}
|
|
@@ -9,7 +9,7 @@ import { HealthClient } from './health.client.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Calling out over `fetch`. Three things a bare `fetch` does not do: retry a 503
|
|
11
11
|
* with backoff, raise a non-2xx as an error carrying the parsed body, and forward
|
|
12
|
-
* the inbound
|
|
12
|
+
* the inbound trace.
|
|
13
13
|
*/
|
|
14
14
|
export class UpstreamDemo {
|
|
15
15
|
constructor(
|
|
@@ -21,8 +21,10 @@ import { UpstreamDemo } from './upstream.demo.js';
|
|
|
21
21
|
retryDelayMs: 20,
|
|
22
22
|
backoff: { jitterMs: 10, maxMs: 200 },
|
|
23
23
|
},
|
|
24
|
-
// The inbound
|
|
25
|
-
|
|
24
|
+
// The inbound trace, forwarded as `traceparent` so one trace spans
|
|
25
|
+
// both services. On by default; stated here because it is the point of
|
|
26
|
+
// this module.
|
|
27
|
+
propagateTrace: true,
|
|
26
28
|
}),
|
|
27
29
|
inject: [AppConfigService] as const,
|
|
28
30
|
}),
|
|
@@ -26,12 +26,12 @@ preload = ["@dunx/transform/preload"]
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
`@dunx/transform` reads each class's constructor parameter types when the file
|
|
29
|
-
loads and records them
|
|
30
|
-
Without it, providers are constructed with no arguments and boot
|
|
31
|
-
|
|
29
|
+
loads and records them. The container uses that record to resolve them before
|
|
30
|
+
calling `new`. Without it, providers are constructed with no arguments, and boot
|
|
31
|
+
fails by naming the problem rather than returning a silent `undefined`.
|
|
32
32
|
|
|
33
33
|
That is also why there is no `@Injectable()` and no `@Inject()`. Being listed in a
|
|
34
|
-
module's `providers` is what makes a class injectable
|
|
34
|
+
module's `providers` is what makes a class injectable. TC39 standard decorators
|
|
35
35
|
have no parameter decorators, so `@Inject()` does not exist.
|
|
36
36
|
|
|
37
37
|
## Next
|