@zerotal/monitor 1.9.0 → 1.11.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 +15 -0
- package/api-surface.md +42 -42
- package/package.json +7 -7
- package/src/MonitorStore.ts +24 -13
- package/src/config.ts +26 -26
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.10.0] — 2026-08-30
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`MonitorStore` options no longer overwrite their own defaults with `undefined`.**
|
|
16
|
+
The constructor applied `?? 100`-style defaults and then spread `...opts` **after**
|
|
17
|
+
them, and object spread copies own properties even when their value is `undefined`.
|
|
18
|
+
So `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config put
|
|
19
|
+
`undefined` straight back over the 7, and `prune()` computed a cutoff of
|
|
20
|
+
`Date.now() - undefined * DAY_MS` — `NaN`, which prunes nothing and reports nothing.
|
|
21
|
+
Spreading last is what made every `??` in that constructor decorative.
|
|
22
|
+
|
|
23
|
+
The caller's options now go first and the defaults fill the gaps, which is the
|
|
24
|
+
ordering `Socket`'s constructor already documents at length for the same reason.
|
|
25
|
+
|
|
11
26
|
## [1.9.0] — 2026-08-29
|
|
12
27
|
|
|
13
28
|
### Added
|
package/api-surface.md
CHANGED
|
@@ -286,32 +286,32 @@ interface ModelStat = {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
interface MonitorConfigShape = {
|
|
289
|
-
accent?: string
|
|
290
|
-
alertCooldownMs?: number
|
|
291
|
-
alertThresholds?: AlertThresholds
|
|
292
|
-
alertWebhook?: string
|
|
293
|
-
alerts?: boolean
|
|
294
|
-
apdexTargetMs?: number
|
|
295
|
-
auth?: (user: unknown) => boolean | Promise<boolean>
|
|
296
|
-
capturePayloads?: boolean
|
|
297
|
-
deploy?: string
|
|
298
|
-
metrics?: boolean
|
|
299
|
-
metricsPath?: string
|
|
300
|
-
path?: string
|
|
301
|
-
payloadMaxBytes?: number
|
|
302
|
-
record?: boolean
|
|
303
|
-
refreshMs?: number
|
|
304
|
-
region?: string
|
|
305
|
-
retentionDays?: number
|
|
306
|
-
retentionMode?: RetentionMode
|
|
307
|
-
sections?: Record<string, boolean>
|
|
308
|
-
slowQueryMs?: number
|
|
309
|
-
slowRequestMs?: number
|
|
310
|
-
snapshotCacheMs?: number
|
|
311
|
-
storage?: string
|
|
312
|
-
subtitle?: string
|
|
313
|
-
title?: string
|
|
314
|
-
zerotalVersion?: string
|
|
289
|
+
accent?: string | undefined
|
|
290
|
+
alertCooldownMs?: number | undefined
|
|
291
|
+
alertThresholds?: AlertThresholds | undefined
|
|
292
|
+
alertWebhook?: string | undefined
|
|
293
|
+
alerts?: boolean | undefined
|
|
294
|
+
apdexTargetMs?: number | undefined
|
|
295
|
+
auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined
|
|
296
|
+
capturePayloads?: boolean | undefined
|
|
297
|
+
deploy?: string | undefined
|
|
298
|
+
metrics?: boolean | undefined
|
|
299
|
+
metricsPath?: string | undefined
|
|
300
|
+
path?: string | undefined
|
|
301
|
+
payloadMaxBytes?: number | undefined
|
|
302
|
+
record?: boolean | undefined
|
|
303
|
+
refreshMs?: number | undefined
|
|
304
|
+
region?: string | undefined
|
|
305
|
+
retentionDays?: number | undefined
|
|
306
|
+
retentionMode?: RetentionMode | undefined
|
|
307
|
+
sections?: Record<string, boolean> | undefined
|
|
308
|
+
slowQueryMs?: number | undefined
|
|
309
|
+
slowRequestMs?: number | undefined
|
|
310
|
+
snapshotCacheMs?: number | undefined
|
|
311
|
+
storage?: string | undefined
|
|
312
|
+
subtitle?: string | undefined
|
|
313
|
+
title?: string | undefined
|
|
314
|
+
zerotalVersion?: string | undefined
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
interface MonitorSection = {
|
|
@@ -386,16 +386,16 @@ interface MonitorStat = {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
interface MonitorStoreOptions = {
|
|
389
|
-
apdexTargetMs?: number
|
|
390
|
-
deploy?: string
|
|
391
|
-
region?: string
|
|
392
|
-
retentionDays?: number
|
|
393
|
-
retentionMode?: RetentionMode
|
|
394
|
-
slowQueryMs?: number
|
|
395
|
-
slowRequestMs?: number
|
|
396
|
-
snapshotCacheMs?: number
|
|
397
|
-
storage?: string
|
|
398
|
-
zerotalVersion?: string
|
|
389
|
+
apdexTargetMs?: number | undefined
|
|
390
|
+
deploy?: string | undefined
|
|
391
|
+
region?: string | undefined
|
|
392
|
+
retentionDays?: number | undefined
|
|
393
|
+
retentionMode?: RetentionMode | undefined
|
|
394
|
+
slowQueryMs?: number | undefined
|
|
395
|
+
slowRequestMs?: number | undefined
|
|
396
|
+
snapshotCacheMs?: number | undefined
|
|
397
|
+
storage?: string | undefined
|
|
398
|
+
zerotalVersion?: string | undefined
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
interface MonitorTable = {
|
|
@@ -525,30 +525,30 @@ interface RequestSpan = {
|
|
|
525
525
|
interface ResolvedMonitorConfig = {
|
|
526
526
|
accent: string
|
|
527
527
|
alertCooldownMs: number
|
|
528
|
-
alertThresholds?: AlertThresholds
|
|
529
|
-
alertWebhook?: string
|
|
528
|
+
alertThresholds?: AlertThresholds | undefined
|
|
529
|
+
alertWebhook?: string | undefined
|
|
530
530
|
alerts: boolean
|
|
531
531
|
apdexTargetMs: number
|
|
532
532
|
auth: (user: unknown) => boolean | Promise<boolean>
|
|
533
533
|
capturePayloads: boolean
|
|
534
|
-
deploy?: string
|
|
534
|
+
deploy?: string | undefined
|
|
535
535
|
metrics: boolean
|
|
536
536
|
metricsPath: string
|
|
537
537
|
path: string
|
|
538
538
|
payloadMaxBytes: number
|
|
539
539
|
record: boolean
|
|
540
540
|
refreshMs: number
|
|
541
|
-
region?: string
|
|
541
|
+
region?: string | undefined
|
|
542
542
|
retentionDays: number
|
|
543
543
|
retentionMode: RetentionMode
|
|
544
|
-
sections?: Record<string, boolean>
|
|
544
|
+
sections?: Record<string, boolean> | undefined
|
|
545
545
|
slowQueryMs: number
|
|
546
546
|
slowRequestMs: number
|
|
547
547
|
snapshotCacheMs: number
|
|
548
548
|
storage: string
|
|
549
549
|
subtitle: string
|
|
550
550
|
title: string
|
|
551
|
-
zerotalVersion?: string
|
|
551
|
+
zerotalVersion?: string | undefined
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
interface RouteDetail = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/monitor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.
|
|
34
|
-
"@zerotal/flow-ui": "1.
|
|
35
|
-
"@zerotal/flow": "1.
|
|
33
|
+
"@zerotal/core": "1.11.0",
|
|
34
|
+
"@zerotal/flow-ui": "1.11.0",
|
|
35
|
+
"@zerotal/flow": "1.11.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@zerotal/queue": "1.
|
|
39
|
-
"@zerotal/scheduler": "1.
|
|
40
|
-
"@zerotal/telemetry": "1.
|
|
38
|
+
"@zerotal/queue": "1.11.0",
|
|
39
|
+
"@zerotal/scheduler": "1.11.0",
|
|
40
|
+
"@zerotal/telemetry": "1.11.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@zerotal/queue": "^1.0.0",
|
package/src/MonitorStore.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* A retention policy (`prune`) deletes or archives data past the configured
|
|
13
13
|
* window, and `wipe` clears everything on demand from the panel.
|
|
14
14
|
*/
|
|
15
|
+
import type { Resolved } from "@zerotal/core";
|
|
15
16
|
import { percentile } from "./store/RingBuffer.ts";
|
|
16
17
|
import { MonitorDb } from "./store/MonitorDb.ts";
|
|
17
18
|
import {
|
|
@@ -107,25 +108,25 @@ interface ReqSample {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
export interface MonitorStoreOptions {
|
|
110
|
-
apdexTargetMs?: number;
|
|
111
|
-
slowQueryMs?: number;
|
|
112
|
-
zerotalVersion?: string;
|
|
113
|
-
region?: string;
|
|
114
|
-
deploy?: string;
|
|
111
|
+
apdexTargetMs?: number | undefined;
|
|
112
|
+
slowQueryMs?: number | undefined;
|
|
113
|
+
zerotalVersion?: string | undefined;
|
|
114
|
+
region?: string | undefined;
|
|
115
|
+
deploy?: string | undefined;
|
|
115
116
|
/** SQLite path. `:memory:` (default) for tests; a file path for persistence. */
|
|
116
|
-
storage?: string;
|
|
117
|
+
storage?: string | undefined;
|
|
117
118
|
/** Days of history to keep before pruning. Default: 7. */
|
|
118
|
-
retentionDays?: number;
|
|
119
|
+
retentionDays?: number | undefined;
|
|
119
120
|
/** What to do with data past retention: delete it or move it to the archive. Default: `delete`. */
|
|
120
|
-
retentionMode?: RetentionMode;
|
|
121
|
+
retentionMode?: RetentionMode | undefined;
|
|
121
122
|
/** Requests at/above this many ms count as "slow". Default: 1000. */
|
|
122
|
-
slowRequestMs?: number;
|
|
123
|
+
slowRequestMs?: number | undefined;
|
|
123
124
|
/**
|
|
124
125
|
* Cache built snapshots for this many ms, keyed by range. De-dupes overlapping
|
|
125
126
|
* builds (panel poll + Prometheus scrape + alert loop all hit "live"). Mutating
|
|
126
127
|
* actions invalidate it. Default: 0 (off) — the provider enables it in production.
|
|
127
128
|
*/
|
|
128
|
-
snapshotCacheMs?: number;
|
|
129
|
+
snapshotCacheMs?: number | undefined;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -139,22 +140,32 @@ export class MonitorStore {
|
|
|
139
140
|
private readonly _snapTtlMs: number;
|
|
140
141
|
private readonly _snapCache = new Map<MonitorRange, { at: number; snap: MonitorSnapshot }>();
|
|
141
142
|
|
|
142
|
-
private readonly _opts:
|
|
143
|
+
private readonly _opts: Resolved<
|
|
143
144
|
Pick<
|
|
144
145
|
MonitorStoreOptions,
|
|
145
146
|
"apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
|
|
146
147
|
>
|
|
147
148
|
> &
|
|
148
|
-
|
|
149
|
+
Omit<
|
|
150
|
+
MonitorStoreOptions,
|
|
151
|
+
"apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
|
|
152
|
+
>;
|
|
149
153
|
|
|
150
154
|
constructor(opts: MonitorStoreOptions = {}) {
|
|
155
|
+
// The caller's options go FIRST, then the defaults fill the gaps — the same
|
|
156
|
+
// ordering, for the same reason, as `Socket`'s constructor documents at length.
|
|
157
|
+
// Object spread copies own properties even when their value is `undefined`, so
|
|
158
|
+
// `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config
|
|
159
|
+
// put `undefined` straight back over the 7 and `prune()` then computed a cutoff
|
|
160
|
+
// of `Date.now() - undefined * DAY_MS` — NaN, which deletes nothing and reports
|
|
161
|
+
// nothing. Spreading last is what made the `??` above decorative.
|
|
151
162
|
this._opts = {
|
|
163
|
+
...opts,
|
|
152
164
|
apdexTargetMs: opts.apdexTargetMs ?? 100,
|
|
153
165
|
slowQueryMs: opts.slowQueryMs ?? 100,
|
|
154
166
|
slowRequestMs: opts.slowRequestMs ?? 1000,
|
|
155
167
|
retentionDays: opts.retentionDays ?? 7,
|
|
156
168
|
retentionMode: opts.retentionMode ?? "delete",
|
|
157
|
-
...opts,
|
|
158
169
|
};
|
|
159
170
|
this._snapTtlMs = Math.max(0, opts.snapshotCacheMs ?? 0);
|
|
160
171
|
this._db = new MonitorDb(opts.storage ?? ":memory:");
|
package/src/config.ts
CHANGED
|
@@ -13,91 +13,91 @@ import type { AlertThresholds } from "./alerting.ts";
|
|
|
13
13
|
|
|
14
14
|
export interface MonitorConfigShape {
|
|
15
15
|
/** URL prefix the panel mounts at. Default: `/monitor`. */
|
|
16
|
-
path?: string;
|
|
16
|
+
path?: string | undefined;
|
|
17
17
|
/** Browser tab title / sidebar heading. Default: `Super Panel`. */
|
|
18
|
-
title?: string;
|
|
18
|
+
title?: string | undefined;
|
|
19
19
|
/** Sub-label under the title. Default: `Zerotal Ops`. */
|
|
20
|
-
subtitle?: string;
|
|
20
|
+
subtitle?: string | undefined;
|
|
21
21
|
/**
|
|
22
22
|
* Authorization gate. Receives the authenticated user (or undefined) and
|
|
23
23
|
* must return true to allow access. Default: allow only outside production.
|
|
24
24
|
*/
|
|
25
|
-
auth?: (user: unknown) => boolean | Promise<boolean
|
|
25
|
+
auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined;
|
|
26
26
|
/** Install the recorder middleware to capture live request data. Default: true. */
|
|
27
|
-
record?: boolean;
|
|
27
|
+
record?: boolean | undefined;
|
|
28
28
|
/** Auto-refresh cadence in milliseconds while "Live" is on. Default: 3000. */
|
|
29
|
-
refreshMs?: number;
|
|
29
|
+
refreshMs?: number | undefined;
|
|
30
30
|
/** Apdex satisfaction threshold (T) in ms. Default: 100. */
|
|
31
|
-
apdexTargetMs?: number;
|
|
31
|
+
apdexTargetMs?: number | undefined;
|
|
32
32
|
/** Queries at/above this many ms count as "slow". Default: 100. */
|
|
33
|
-
slowQueryMs?: number;
|
|
33
|
+
slowQueryMs?: number | undefined;
|
|
34
34
|
/** Requests at/above this many ms count as "slow" (Slow Requests widget). Default: 1000. */
|
|
35
|
-
slowRequestMs?: number;
|
|
35
|
+
slowRequestMs?: number | undefined;
|
|
36
36
|
/**
|
|
37
37
|
* Capture request/response headers and bodies on each request (Telescope-style),
|
|
38
38
|
* shown in the request trace. Off by default — it buffers bodies and is
|
|
39
39
|
* privacy-sensitive. Sensitive headers (authorization/cookie) and body keys
|
|
40
40
|
* (password/token/secret/…) are redacted automatically.
|
|
41
41
|
*/
|
|
42
|
-
capturePayloads?: boolean;
|
|
42
|
+
capturePayloads?: boolean | undefined;
|
|
43
43
|
/** Truncate captured request/response bodies to this many bytes. Default: 65536. */
|
|
44
|
-
payloadMaxBytes?: number;
|
|
44
|
+
payloadMaxBytes?: number | undefined;
|
|
45
45
|
/**
|
|
46
46
|
* Cache built snapshots for this many ms, keyed by range, so overlapping reads
|
|
47
47
|
* (panel poll + Prometheus scrape + alert loop) share one build. Mutating actions
|
|
48
48
|
* invalidate it. Default: 1000. Set `0` to always build fresh.
|
|
49
49
|
*/
|
|
50
|
-
snapshotCacheMs?: number;
|
|
50
|
+
snapshotCacheMs?: number | undefined;
|
|
51
51
|
/**
|
|
52
52
|
* SQLite file the panel persists to, so history survives restarts and the
|
|
53
53
|
* 1h/24h/7d ranges trace real data. Default: `storage/monitor.sqlite`. Use
|
|
54
54
|
* `:memory:` for an ephemeral, in-process store.
|
|
55
55
|
*/
|
|
56
|
-
storage?: string;
|
|
56
|
+
storage?: string | undefined;
|
|
57
57
|
/** Days of history to keep before pruning. Default: 7. */
|
|
58
|
-
retentionDays?: number;
|
|
58
|
+
retentionDays?: number | undefined;
|
|
59
59
|
/** What to do with data past retention: `delete` it or `archive` it. Default: `delete`. */
|
|
60
|
-
retentionMode?: RetentionMode;
|
|
60
|
+
retentionMode?: RetentionMode | undefined;
|
|
61
61
|
/**
|
|
62
62
|
* Expose a Prometheus text-exposition endpoint. Default: `false` (opt-in).
|
|
63
63
|
* The endpoint is unauthenticated (a scraper can't satisfy user-auth), so it
|
|
64
64
|
* ships off; enable it only when you protect `metricsPath` at the network
|
|
65
65
|
* layer (firewall/ingress) or scope it to a private interface.
|
|
66
66
|
*/
|
|
67
|
-
metrics?: boolean;
|
|
67
|
+
metrics?: boolean | undefined;
|
|
68
68
|
/** Path for the Prometheus endpoint. Default: `/metrics`. Protect it at the network layer. */
|
|
69
|
-
metricsPath?: string;
|
|
69
|
+
metricsPath?: string | undefined;
|
|
70
70
|
/** Evaluate threshold alerts on a short interval. Default: `true`. */
|
|
71
|
-
alerts?: boolean;
|
|
71
|
+
alerts?: boolean | undefined;
|
|
72
72
|
/** Alert thresholds (error rate, queue backlog, p95, rollbacks). */
|
|
73
|
-
alertThresholds?: AlertThresholds;
|
|
73
|
+
alertThresholds?: AlertThresholds | undefined;
|
|
74
74
|
/**
|
|
75
75
|
* Minimum gap (ms) before the same alert can fire again after it recovers and
|
|
76
76
|
* re-breaches. Stops an oscillating metric from re-paging / re-flooding the feed
|
|
77
77
|
* for one ongoing issue. Default: 30 min. Set `0` to fire on every fresh breach.
|
|
78
78
|
*/
|
|
79
|
-
alertCooldownMs?: number;
|
|
79
|
+
alertCooldownMs?: number | undefined;
|
|
80
80
|
/**
|
|
81
81
|
* Slack-compatible webhook URL. When set, every newly-firing alert is POSTed to
|
|
82
82
|
* it as JSON (`{ text, level, title, detail }`) so alerts page someone instead of
|
|
83
83
|
* only lighting up the panel. Dependency-free (uses `fetch`); delivery is
|
|
84
84
|
* best-effort. For richer routing, register a handler with `onAlert()` instead.
|
|
85
85
|
*/
|
|
86
|
-
alertWebhook?: string;
|
|
86
|
+
alertWebhook?: string | undefined;
|
|
87
87
|
/** Accent colour (hex) for the panel chrome. Default: Zerotal orange. */
|
|
88
|
-
accent?: string;
|
|
88
|
+
accent?: string | undefined;
|
|
89
89
|
/** Version string shown in the System footer. */
|
|
90
|
-
zerotalVersion?: string;
|
|
90
|
+
zerotalVersion?: string | undefined;
|
|
91
91
|
/** Region label shown in the System footer. */
|
|
92
|
-
region?: string;
|
|
92
|
+
region?: string | undefined;
|
|
93
93
|
/** Deploy SHA shown in the System footer. */
|
|
94
|
-
deploy?: string;
|
|
94
|
+
deploy?: string | undefined;
|
|
95
95
|
/**
|
|
96
96
|
* Switch contributed sections off by id — `{ scheduler: false }` keeps the
|
|
97
97
|
* scheduler installed but drops its section from the panel. Anything absent
|
|
98
98
|
* here is on.
|
|
99
99
|
*/
|
|
100
|
-
sections?: Record<string, boolean
|
|
100
|
+
sections?: Record<string, boolean> | undefined;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface ResolvedMonitorConfig extends MonitorConfigShape {
|