@dunx/dashboard 3.1.3 → 3.2.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/README.md CHANGED
@@ -43,7 +43,7 @@ app.use(DashboardMiddleware); // first, ahead of any session guard
43
43
 
44
44
  ## The panels
45
45
 
46
- Six panels, each with a JSON sibling under `{path}/api/*`, so `curl` is a real
46
+ Seven panels, each with a JSON sibling under `{path}/api/*`, so `curl` is a real
47
47
  way to read this on a box with no browser. The queues page is bull-board's.
48
48
 
49
49
  | Panel | Shows |
@@ -54,11 +54,12 @@ way to read this on a box with no browser. The queues page is bull-board's.
54
54
  | Redis | Connection state and `INFO` |
55
55
  | Config | Keys always, values only where `reveal` says so |
56
56
  | Runtime | The process, its memory and its probes |
57
+ | Stats | Per-route request timings and per-operation query timings |
57
58
 
58
59
  ## Three things that are decisions
59
60
 
60
- - **`authorize` has no default**, and leaving it out serves the page to anyone
61
- who can reach the port. Omitting it logs a warning naming the mount at boot.
61
+ - **`authorize` has no default.** Leaving it out serves the page to anyone who
62
+ can reach the port. Omitting it logs a warning naming the mount at boot.
62
63
  - **A rejected request gets 404, not 403.** Register the middleware **ahead of
63
64
  any session guard**: a guard running first answers 401 and tells a prober the
64
65
  mount exists. `authorize` takes the raw `Request` so it can be self-sufficient.
@@ -67,9 +68,9 @@ way to read this on a box with no browser. The queues page is bull-board's.
67
68
 
68
69
  ## Notes
69
70
 
70
- - It depends on `@dunx/infra` and `bullmq` not at all. `QueueSource` and
71
- `RedisProbe` restate structurally what `JobPublisher` and `RedisConnection`
72
- already are, so `queues: publisher` is the whole wiring.
71
+ - `QueueSource` and `RedisProbe` restate structurally what `JobPublisher` and
72
+ `RedisConnection` already are, so `queues: publisher` is the whole wiring.
73
+ It depends on neither `@dunx/infra` nor `bullmq`.
73
74
  - The board is built on the first request for the queues page, never at boot, so
74
75
  an app that never opens it holds no broker socket.
75
76
  - `commands: false` maps onto bull-board's own `readOnlyMode`.
@@ -1,6 +1,6 @@
1
- import type { ModuleNode, ProviderNode } from '@dunx/core';
1
+ import type { MemoryReport, ModuleNode, ProviderNode, RuntimeReport as CoreRuntimeReport } from '@dunx/core';
2
2
  import type { GatewayNode, RouteNode } from '@dunx/http/internal';
3
- import type { ProbeState } from '../contracts.js';
3
+ import type { DbQueryStats, DbStatsReport, HistogramSnapshot, HttpStatsReport, ProbeState, RouteStats } from '../contracts.js';
4
4
  /**
5
5
  * Everything the page reads, declared once.
6
6
  *
@@ -61,23 +61,19 @@ export interface ProbeReport {
61
61
  /** How long the probe took, so a slow dependency is visible before it fails. */
62
62
  readonly ms: number;
63
63
  }
64
- export interface MemoryReport {
65
- readonly rss: number;
66
- readonly heapUsed: number;
67
- readonly heapTotal: number;
68
- readonly external: number;
69
- }
64
+ /**
65
+ * The process half moved down to `@dunx/core` once the health module wanted the
66
+ * same numbers. Re-exported so `internal/dashboard-ui`'s relative `import type`
67
+ * is unchanged, and so nothing here declares a second copy of it.
68
+ */
69
+ export type { CoreRuntimeReport, MemoryReport };
70
70
  /** The half that changes. Polled. */
71
- export interface RuntimeReport {
72
- readonly pid: number;
73
- readonly uptimeMs: number;
74
- readonly bun: string;
75
- readonly platform: string;
76
- readonly arch: string;
77
- readonly memory: MemoryReport;
71
+ export interface RuntimeReport extends Omit<CoreRuntimeReport, 'now' | 'cpu' | 'resource'> {
78
72
  readonly probes: readonly ProbeReport[];
79
- /** Server clock, so the page can show ages rather than raw timestamps. */
73
+ /** Server clock as epoch milliseconds, so the page can show ages. */
80
74
  readonly now: number;
75
+ readonly cpu: CoreRuntimeReport['cpu'];
76
+ readonly resource: CoreRuntimeReport['resource'];
81
77
  }
82
78
  export interface RedisReport {
83
79
  readonly configured: true;
@@ -108,3 +104,24 @@ export interface QueuesReport {
108
104
  /** Why there is no board: no source, no queues opened, or bull-board absent. */
109
105
  readonly unavailable?: string;
110
106
  }
107
+ /** Absent because the app passed no source, which the page reads to hide a panel. */
108
+ export interface StatsAbsent {
109
+ readonly configured: false;
110
+ }
111
+ /**
112
+ * A half that is present carries `configured: true`, the same discriminant
113
+ * `RedisReport` uses: the reports themselves are `@dunx/http`'s and a restatement
114
+ * of `@dunx/infra/db`'s, and neither should grow a field only this page reads.
115
+ */
116
+ export type StatsHalf<T> = (T & {
117
+ readonly configured: true;
118
+ }) | StatsAbsent;
119
+ /**
120
+ * Both halves of `{path}/api/stats`. Independent: an app may time requests and
121
+ * not queries, or the other way round.
122
+ */
123
+ export interface StatsReport {
124
+ readonly http: StatsHalf<HttpStatsReport>;
125
+ readonly db: StatsHalf<DbStatsReport>;
126
+ }
127
+ export type { DbQueryStats, DbStatsReport, HistogramSnapshot, HttpStatsReport, RouteStats, };
@@ -1,3 +1,5 @@
1
+ import type { HistogramSnapshot } from '@dunx/core';
2
+ import type { HttpStatsReport, ProbeResult } from '@dunx/http';
1
3
  /**
2
4
  * What the dashboard needs from the things it reports on, restated structurally
3
5
  * so this package depends on `@dunx/infra` and `bullmq` not at all. Each is
@@ -8,6 +10,8 @@
8
10
  * | `QueueSource` | `JobPublisher` from `@dunx/infra/queue` |
9
11
  * | `RedisProbe` | `RedisConnection` from `@dunx/infra/redis` |
10
12
  * | `ConfigValues` | `ConfigService` from `@dunx/core` |
13
+ * | `StatsSource` | `RequestMetrics` from `@dunx/http` |
14
+ * | `DbStatsSource`| `QueryMetrics` from `@dunx/infra/db` |
11
15
  */
12
16
  /**
13
17
  * The validated configuration. `ConfigService` satisfies it as written. Passed in
@@ -30,6 +34,39 @@ export interface QueueSource {
30
34
  * `unknown` rather than a restatement. */
31
35
  queue(name: string): unknown;
32
36
  }
37
+ /**
38
+ * Per-route request counts and timings. `RequestMetrics` from `@dunx/http`
39
+ * satisfies it as written, and `HttpStatsReport` is imported rather than
40
+ * restated: this package already peer-depends on `@dunx/http`, so a second copy
41
+ * of `RouteStats` here would be a second thing to keep in step.
42
+ */
43
+ export interface StatsSource {
44
+ snapshot(): HttpStatsReport;
45
+ }
46
+ export type { HistogramSnapshot } from '@dunx/core';
47
+ export type { HttpStatsReport, RouteStats } from '@dunx/http';
48
+ /**
49
+ * Query counts and timings. `QueryMetrics` from `@dunx/infra/db` satisfies it
50
+ * structurally, and `DbStatsReport` **is** restated here for the reason
51
+ * `QueueSource` is: this package depends on `@dunx/infra` not at all, and a peer
52
+ * on it for one report type would be the dependency the boundary exists to
53
+ * refuse. Narrowing a field silently un-satisfies `QueryMetrics`.
54
+ */
55
+ export interface DbStatsSource {
56
+ snapshot(): DbStatsReport;
57
+ }
58
+ export interface DbQueryStats {
59
+ readonly operation: string;
60
+ readonly count: number;
61
+ readonly errors: number;
62
+ readonly duration: HistogramSnapshot;
63
+ readonly slowest?: string;
64
+ }
65
+ export interface DbStatsReport {
66
+ readonly operations: readonly DbQueryStats[];
67
+ readonly total: number;
68
+ readonly since: string;
69
+ }
33
70
  /**
34
71
  * Enough Redis to answer "is it up and what is it doing". `send` rather than a
35
72
  * typed `info()`: a method per command is how a restatement becomes a client.
@@ -45,7 +82,6 @@ export interface RedisProbe {
45
82
  * it. `RedisProbe` below did not move with them: `PingProbe` is a `ping` alone.
46
83
  */
47
84
  export type { ProbeResult, ProbeState } from '@dunx/http';
48
- import type { ProbeResult } from '@dunx/http';
49
85
  /**
50
86
  * Anything else worth a light on the page. Awaited with a timeout and never let
51
87
  * to throw, so a probe that hangs costs one panel rather than the page.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { DashboardModule } from './module.js';
2
2
  export { DashboardMiddleware } from './middleware.js';
3
3
  export { DashboardOptions, normalizeMount, type Authorize, type DashboardOptionsInit, type Reveal, } from './options.js';
4
- export type { ConfigValues, DashboardProbe, ProbeResult, ProbeState, QueueSource, RedisProbe, } from './contracts.js';
5
- export type { ConfigEntry, GatewayNode, MemoryReport, Meta, ModuleNode, ProbeReport, ProviderNode, QueuesReport, RedisAbsent, RedisReport, RouteNode, RuntimeReport, Snapshot, } from './api/types.js';
4
+ export type { ConfigValues, DashboardProbe, DbStatsSource, ProbeResult, ProbeState, QueueSource, RedisProbe, StatsSource, } from './contracts.js';
5
+ export type { ConfigEntry, GatewayNode, DbQueryStats, DbStatsReport, HistogramSnapshot, HttpStatsReport, MemoryReport, Meta, ModuleNode, ProbeReport, RouteStats, StatsAbsent, StatsHalf, StatsReport, ProviderNode, QueuesReport, RedisAbsent, RedisReport, RouteNode, RuntimeReport, Snapshot, } from './api/types.js';
6
6
  export { handleDashboard, type RouterDeps } from './router.js';
package/dist/index.js CHANGED
@@ -145,6 +145,8 @@ class DashboardOptions {
145
145
  queueNames;
146
146
  redis;
147
147
  probes;
148
+ stats;
149
+ dbStats;
148
150
  config;
149
151
  reveal;
150
152
  openApiPath;
@@ -159,6 +161,8 @@ class DashboardOptions {
159
161
  this.queueNames = init.queueNames ?? [];
160
162
  this.redis = init.redis;
161
163
  this.probes = init.probes ?? [];
164
+ this.stats = init.stats;
165
+ this.dbStats = init.dbStats;
162
166
  this.config = init.config;
163
167
  this.reveal = init.reveal ?? (() => false);
164
168
  this.openApiPath = init.openApiPath;
@@ -251,6 +255,7 @@ var redisReport = async (redis, timeoutMs) => {
251
255
  };
252
256
 
253
257
  // src/api/runtime.ts
258
+ import { RuntimeStats } from "@dunx/core";
254
259
  var withTimeout = (probe, ms) => bounded(async () => {
255
260
  try {
256
261
  return await probe.check();
@@ -282,27 +287,14 @@ var redisProbe = (redis) => ({
282
287
  };
283
288
  }
284
289
  });
285
- var memory = () => {
286
- const usage = process.memoryUsage();
287
- return {
288
- rss: usage.rss,
289
- heapUsed: usage.heapUsed,
290
- heapTotal: usage.heapTotal,
291
- external: usage.external
292
- };
293
- };
294
290
  var runtimeReport = async (options, startedAt) => {
295
291
  const probes = [
296
292
  ...options.redis ? [redisProbe(options.redis)] : [],
297
293
  ...options.probes
298
294
  ];
295
+ const { now: _iso, ...process_ } = new RuntimeStats(startedAt).snapshot();
299
296
  return {
300
- pid: process.pid,
301
- uptimeMs: Math.round(performance.now() - startedAt),
302
- bun: Bun.version,
303
- platform: process.platform,
304
- arch: process.arch,
305
- memory: memory(),
297
+ ...process_,
306
298
  probes: await Promise.all(probes.map((probe) => runProbe(probe, options.probeTimeoutMs))),
307
299
  now: Date.now()
308
300
  };
@@ -324,6 +316,13 @@ var handleApi = async (deps, method, segments) => {
324
316
  return json(await runtimeReport(deps.options, deps.startedAt));
325
317
  case "redis":
326
318
  return json(deps.options.redis === undefined ? { configured: false } : await redisReport(deps.options.redis, deps.options.probeTimeoutMs));
319
+ case "stats": {
320
+ const report = {
321
+ http: deps.options.stats === undefined ? { configured: false } : { ...deps.options.stats.snapshot(), configured: true },
322
+ db: deps.options.dbStats === undefined ? { configured: false } : { ...deps.options.dbStats.snapshot(), configured: true }
323
+ };
324
+ return json(report);
325
+ }
327
326
  case "queues": {
328
327
  const { names, unavailable } = boardNames(deps.options);
329
328
  return json({
package/dist/options.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BunRequest } from 'bun';
2
- import type { ConfigValues, DashboardProbe, QueueSource, RedisProbe } from './contracts.js';
2
+ import type { ConfigValues, DashboardProbe, DbStatsSource, QueueSource, RedisProbe, StatsSource } from './contracts.js';
3
3
  /**
4
4
  * Decides whether a request may see the dashboard at all. It receives the raw
5
5
  * `Request`: the middleware must be registered ahead of any session guard, so
@@ -44,6 +44,12 @@ export interface DashboardOptionsInit {
44
44
  readonly redis?: RedisProbe;
45
45
  /** Anything else worth a light: a database, an upstream, a leader lease. */
46
46
  readonly probes?: readonly DashboardProbe[];
47
+ /** `RequestMetrics` from `@dunx/http` goes here, and needs `metrics: true` on
48
+ * `HttpFactory.create` to have anything in it. Absent means no stats panel. */
49
+ readonly stats?: StatsSource;
50
+ /** `QueryMetrics` from `@dunx/infra/db` goes here, and needs
51
+ * `DbModule.forRoot(options, { metrics: true })`. */
52
+ readonly dbStats?: DbStatsSource;
47
53
  /** `ConfigService` goes here; the panel is absent without it. */
48
54
  readonly config?: ConfigValues;
49
55
  /** See {@link Reveal}. The default reveals nothing, even with `config` set. */
@@ -83,6 +89,8 @@ export declare class DashboardOptions {
83
89
  readonly queueNames: readonly string[];
84
90
  readonly redis: RedisProbe | undefined;
85
91
  readonly probes: readonly DashboardProbe[];
92
+ readonly stats: StatsSource | undefined;
93
+ readonly dbStats: DbStatsSource | undefined;
86
94
  readonly config: ConfigValues | undefined;
87
95
  readonly reveal: Reveal;
88
96
  readonly openApiPath: string | undefined;