@worker-manager/metrics 1.0.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 +136 -0
- package/dist/HistoryAdmin.d.ts +134 -0
- package/dist/HistoryAdmin.js +345 -0
- package/dist/HistoryAdmin.js.map +1 -0
- package/dist/HistoryStore.d.ts +37 -0
- package/dist/HistoryStore.js +140 -0
- package/dist/HistoryStore.js.map +1 -0
- package/dist/LatencySampler.d.ts +88 -0
- package/dist/LatencySampler.js +285 -0
- package/dist/LatencySampler.js.map +1 -0
- package/dist/LatencyStore.d.ts +21 -0
- package/dist/LatencyStore.js +218 -0
- package/dist/LatencyStore.js.map +1 -0
- package/dist/MetricsRecorder.d.ts +92 -0
- package/dist/MetricsRecorder.js +148 -0
- package/dist/MetricsRecorder.js.map +1 -0
- package/dist/RedisMetricsHistoryProvider.d.ts +28 -0
- package/dist/RedisMetricsHistoryProvider.js +147 -0
- package/dist/RedisMetricsHistoryProvider.js.map +1 -0
- package/dist/connection.d.ts +10 -0
- package/dist/connection.js +26 -0
- package/dist/connection.js.map +1 -0
- package/dist/dataMapping.d.ts +13 -0
- package/dist/dataMapping.js +31 -0
- package/dist/dataMapping.js.map +1 -0
- package/dist/histogram.d.ts +27 -0
- package/dist/histogram.js +101 -0
- package/dist/histogram.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +21 -0
- package/dist/keys.js +76 -0
- package/dist/keys.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @worker-manager/metrics
|
|
2
|
+
|
|
3
|
+
> Status: Beta. The API and Redis storage layout may still change in a minor release while the feature settles. It is safe to run (opt-in, and it only writes its own namespaced keys), but pin an exact version if you depend on the storage format.
|
|
4
|
+
|
|
5
|
+
Opt-in long-retention historical job metrics for [Worker Manager](https://github.com/naldomadeira/worker-manager).
|
|
6
|
+
|
|
7
|
+
Snapshots native BullMQ per-minute metrics into long-retention Redis buckets and exposes a
|
|
8
|
+
`MetricsHistoryProvider` that feeds bull-board's history charts. Everything is opt-in: the core
|
|
9
|
+
`@worker-manager/api` stays stateless.
|
|
10
|
+
|
|
11
|
+
## Precondition
|
|
12
|
+
|
|
13
|
+
Your BullMQ workers must have native metrics enabled, with a window large enough to survive any
|
|
14
|
+
recorder downtime, for example:
|
|
15
|
+
|
|
16
|
+
new Worker(name, processor, {
|
|
17
|
+
connection,
|
|
18
|
+
metrics: { maxDataPoints: MetricsTime.ONE_WEEK },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
import { MetricsRecorder, RedisMetricsHistoryProvider } from '@worker-manager/metrics';
|
|
24
|
+
|
|
25
|
+
// In your always-on worker/app process:
|
|
26
|
+
const recorder = new MetricsRecorder({
|
|
27
|
+
queues: [new BullMQAdapter(queue)],
|
|
28
|
+
connection,
|
|
29
|
+
retentionDays: 90,
|
|
30
|
+
});
|
|
31
|
+
recorder.start();
|
|
32
|
+
|
|
33
|
+
// Where you build the board:
|
|
34
|
+
createBullBoard({
|
|
35
|
+
queues,
|
|
36
|
+
serverAdapter,
|
|
37
|
+
options: { historyProvider: new RedisMetricsHistoryProvider({ connection }) },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
`queues` also accepts a function, resolved on every tick instead of once, which is what you want when the queue set changes while the recorder runs.
|
|
41
|
+
|
|
42
|
+
Not embedding bull-board in an app of your own? This package ships inside [`@worker-manager/cli`](https://www.npmjs.com/package/@worker-manager/cli) and the `ghcr.io/naldomadeira/worker-manager` image, where `--history` registers the provider and starts a recorder in the same process. See the [CLI guide](https://naldomadeira.github.io/worker-manager/guide/cli#historical-metrics).
|
|
43
|
+
|
|
44
|
+
On shutdown, call `recorder.stop()` and `provider.disconnect()`. Both only close the Redis connection if the recorder/provider opened it internally, so it's a safe no-op if you passed in your own `Redis` instance.
|
|
45
|
+
|
|
46
|
+
`connection` may be ioredis options, or a `Redis` or `Cluster` instance you created. `ioredis` is a peer dependency (v5 or v6): resolve a single copy in your app, and if you reuse an existing client, pass one built from that same `ioredis`. A client from a different install (for example one created internally by a BullMQ pinned to a different ioredis major) is not recognized as a client and would be misread as options.
|
|
47
|
+
|
|
48
|
+
Timestamps and buckets are UTC.
|
|
49
|
+
|
|
50
|
+
## Key namespace
|
|
51
|
+
|
|
52
|
+
Every key the recorder writes lives under `bull-board:metrics:`. Pass `prefix` to move it, which is how two boards share one Redis without their histories running together:
|
|
53
|
+
|
|
54
|
+
const recorder = new MetricsRecorder({ queues, connection, prefix: 'staging:metrics' });
|
|
55
|
+
const provider = new RedisMetricsHistoryProvider({ connection, prefix: 'staging:metrics' });
|
|
56
|
+
|
|
57
|
+
The provider, the recorder and any `MetricsHistoryAdmin` must all be given the same prefix. A provider reading a namespace nothing writes to reports empty history rather than an error, the same way a mismatched retention quietly shortens the window.
|
|
58
|
+
|
|
59
|
+
## Redis Cluster
|
|
60
|
+
|
|
61
|
+
Pass a `Cluster` as `connection` and it works, with one thing worth knowing about the key layout.
|
|
62
|
+
|
|
63
|
+
Each snapshot writes a queue's three tiers and the three `__global__` rollup tiers in a single `EVAL`, which is what makes the write idempotent across all resolutions at once. Redis Cluster rejects a multi-key command whose keys land in different slots, so the whole namespace has to hash to one slot. It is given a [hash tag](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/#hash-tags) for that: `bull-board:metrics` becomes `{bull-board:metrics}`, and a `prefix` of your own is wrapped the same way unless it already carries a `{...}` tag, in which case yours is used and you choose the slot.
|
|
64
|
+
|
|
65
|
+
One slot means one master holds the history for the whole board. That is the trade for keeping the rollup consistent on write rather than recomputing it on read, and the volume is the same as the storage section below: roughly 50 MB at 200 busy queues, and one `EVAL` per queue per metric per minute.
|
|
66
|
+
|
|
67
|
+
Standalone keys are untagged and unchanged, so nothing moves for an existing deployment. Nothing carries over from a standalone Redis to a cluster, since the key names differ.
|
|
68
|
+
|
|
69
|
+
Latency sampling reads BullMQ's own keys through the same connection, so your queues need the hash-tagged prefix BullMQ already asks for in cluster mode (`new Queue(name, { prefix: '{bull}' })`). Without it the sampler's pipelines span slots; it swallows that error, so pass `onLatencyError` to see it.
|
|
70
|
+
|
|
71
|
+
The CLI and the Docker image reach a cluster with `--cluster`, where `--history` works the same way.
|
|
72
|
+
|
|
73
|
+
## Job latency
|
|
74
|
+
|
|
75
|
+
Alongside the completed/failed counters, the recorder tracks two histograms per queue: wait time (`processedOn - timestamp`, how long a job sat before a worker picked it up) and run time (`finishedOn - processedOn`, how long the handler took). They diagnose different problems, so they're kept separate rather than combined into one number.
|
|
76
|
+
|
|
77
|
+
Both are collected by scanning the completed and failed sorted sets (BullMQ scores them by finish time via `moveToFinished`'s `ZADD`) past a watermark on the recorder's existing tick, so no worker changes are needed and there's no precondition on `queue.getMetrics()`.
|
|
78
|
+
|
|
79
|
+
The wait histogram only sees jobs that finished, so it goes quiet exactly when a queue is backed up and jobs stop finishing. A queue-age gauge (oldest job still waiting) is recorded alongside it for that reason, and the UI overlays it on the wait chart. Retries are excluded from wait time only, since `timestamp` is a job's creation but `processedOn` is its latest attempt. Percentiles are estimates bounded by bucket width; the bucket layout is fixed, not configurable, because two ranges with different layouts can't be merged into one percentile.
|
|
80
|
+
|
|
81
|
+
`removeOnComplete: true` deletes jobs the instant they finish, so there's nothing left to scan; that queue will never show latency data. Deleting, cleaning, or retrying jobs by hand does the same to whatever finished since the last tick. The counter charts are unaffected, since BullMQ counts a job as it finishes and never decrements when it is removed. Latency sampling is on by default; set `latency: false` on `MetricsRecorder` to turn it off.
|
|
82
|
+
|
|
83
|
+
## PostgreSQL-backed queues
|
|
84
|
+
|
|
85
|
+
A BullMQ 6 queue backed by PostgreSQL records no history. Its `getMetrics()` reports `prevTS` as 0, which leaves the per-minute buffer undatable, so the counters are dropped rather than dated from the recorder's clock; the field is tracked in the backend's schema, so this may resolve upstream. Latency sampling needs BullMQ's Redis keys, which such a queue does not have, so it is skipped rather than recorded as an empty backlog. Redis-backed queues on the same board are unaffected.
|
|
86
|
+
|
|
87
|
+
## Storage
|
|
88
|
+
|
|
89
|
+
Each snapshot is written at three resolutions at once, each with its own retention, because they cost very different amounts:
|
|
90
|
+
|
|
91
|
+
| Tier | Default retention | Size per busy day, per queue and metric |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| Minute | 7 days | ~72 KB |
|
|
94
|
+
| Hour | 90 days | ~0.3 KB |
|
|
95
|
+
| Day | 90 days | ~15 bytes |
|
|
96
|
+
|
|
97
|
+
At the defaults that's roughly 1.1 MB for a queue busy every minute of every day across both metrics, and far less for a bursty one. Minutes with no activity are never written, so the footprint follows how busy a queue is, not how long it has been recording.
|
|
98
|
+
|
|
99
|
+
const recorder = new MetricsRecorder({
|
|
100
|
+
queues,
|
|
101
|
+
connection,
|
|
102
|
+
retention: { minutes: 7, hours: 90, days: 90 },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
The minute window is the one worth tuning: it holds essentially all the bytes, and it doubles as the recorder's catch-up window after downtime. `retentionDays: N` still works and sets the hourly and daily windows, leaving the minute window at its default.
|
|
106
|
+
|
|
107
|
+
Latency histograms and the queue-age gauge use a separate packed format and are measured separately, at 90 day retention:
|
|
108
|
+
|
|
109
|
+
| Scenario | Measured |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| One queue, both histograms plus the queue-age gauge, realistic distribution | 254.5 KB |
|
|
112
|
+
| Same, pathological: all 18 buckets populated heavily every hour | 574.6 KB |
|
|
113
|
+
| Shared `__global__` cross-queue rollup | ~224 KB once, for the whole board |
|
|
114
|
+
|
|
115
|
+
That rollup is a single shared cost, not multiplied per queue: 200 queues at typical traffic is roughly 200 × 254.5 KB, about 50 MB, plus the one shared 224 KB rollup. `sample()` takes about 9.11 ms per tick at 1000 finished jobs and about 12 Redis round trips per queue per tick, flat in job count; a subsampling cap above `maxLatencySamplesPerTick` keeps that bounded even at 10,000 jobs a tick.
|
|
116
|
+
|
|
117
|
+
Retention is enforced by Redis. Day-scoped keys expire on their own TTL; the daily totals hashes are trimmed to the window as each new day rolls in.
|
|
118
|
+
|
|
119
|
+
## Inspecting and clearing history
|
|
120
|
+
|
|
121
|
+
import { MetricsHistoryAdmin } from '@worker-manager/metrics';
|
|
122
|
+
|
|
123
|
+
const admin = new MetricsHistoryAdmin({ connection }); // add `prefix` if the recorder has one
|
|
124
|
+
|
|
125
|
+
await admin.stats(); // bytes per tier and per queue, day range
|
|
126
|
+
await admin.purge(); // delete everything
|
|
127
|
+
await admin.purge({ queue: 'mailer' }); // delete one queue
|
|
128
|
+
await admin.purge({ before: '2026-06-01' }); // delete anything older than a day
|
|
129
|
+
|
|
130
|
+
Both are `SCAN`-driven and confined to this package's namespace, so they never block Redis and never touch BullMQ's own keys. On a cluster they scan every master, since `SCAN` carries no key for the client to route by. Purging a single queue also subtracts it from the cross-queue rollup. Call `admin.disconnect()` when done.
|
|
131
|
+
|
|
132
|
+
`RedisMetricsHistoryProvider` exposes the same two operations to the board, which turns them into a storage panel on the Metrics history page with a confirmation before anything is deleted.
|
|
133
|
+
|
|
134
|
+
## Scope
|
|
135
|
+
|
|
136
|
+
The shipped Worker Manager UI reads daily rollups. `getHistory` also supports hourly granularity for custom consumers (via the core's `/api/metrics/history` endpoint), though the built-in charts don't use it.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type MetricsConnection } from './connection';
|
|
2
|
+
export interface TierStats {
|
|
3
|
+
keys: number;
|
|
4
|
+
/** Sum of `MEMORY USAGE` over this tier's keys, in bytes. */
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface HistoryQueueStats {
|
|
8
|
+
/** Queue name, or `__global__` for the cross-queue rollup. */
|
|
9
|
+
queue: string;
|
|
10
|
+
keys: number;
|
|
11
|
+
bytes: number;
|
|
12
|
+
/** Recorded minute buckets, the tier that drives storage size. */
|
|
13
|
+
minutes: number;
|
|
14
|
+
/** Days covered by a minute or hour hash, ascending. */
|
|
15
|
+
days: string[];
|
|
16
|
+
/** Where this queue's bytes actually sit, so a footprint can be diagnosed. */
|
|
17
|
+
tiers: Record<HistoryTier, TierStats>;
|
|
18
|
+
}
|
|
19
|
+
export interface HistoryStats {
|
|
20
|
+
keys: number;
|
|
21
|
+
bytes: number;
|
|
22
|
+
minutes: number;
|
|
23
|
+
oldestDay: string | null;
|
|
24
|
+
newestDay: string | null;
|
|
25
|
+
tiers: Record<HistoryTier, TierStats>;
|
|
26
|
+
queues: HistoryQueueStats[];
|
|
27
|
+
}
|
|
28
|
+
export interface PurgeOptions {
|
|
29
|
+
/** Limit the purge to one queue. Omit to purge every queue plus the global rollup. */
|
|
30
|
+
queue?: string;
|
|
31
|
+
/** Only drop days strictly before this date (UTC). Omit to drop everything in scope. */
|
|
32
|
+
before?: Date | string;
|
|
33
|
+
}
|
|
34
|
+
export interface PurgeResult {
|
|
35
|
+
keysDeleted: number;
|
|
36
|
+
/** Day fields removed from totals hashes. */
|
|
37
|
+
fieldsDeleted: number;
|
|
38
|
+
}
|
|
39
|
+
export interface MetricsHistoryAdminOptions {
|
|
40
|
+
connection: MetricsConnection;
|
|
41
|
+
/** Must match the recorder's. See `MetricsRecorderOptions.prefix`. */
|
|
42
|
+
prefix?: string;
|
|
43
|
+
}
|
|
44
|
+
export type HistoryTier = 'minute' | 'hour' | 'day';
|
|
45
|
+
interface ParsedKey {
|
|
46
|
+
queue: string;
|
|
47
|
+
metric: string;
|
|
48
|
+
tier: HistoryTier;
|
|
49
|
+
/** ISO day the key covers, `null` for the daily totals hash. */
|
|
50
|
+
day: string | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parses the three key shapes from the right, because queue names may themselves contain
|
|
54
|
+
* colons:
|
|
55
|
+
*
|
|
56
|
+
* <ns>:<queue>:<metric>:<day> minute buckets
|
|
57
|
+
* <ns>:<queue>:<metric>:hour:<day> hourly rollup
|
|
58
|
+
* <ns>:<queue>:<metric>:totals daily totals
|
|
59
|
+
*
|
|
60
|
+
* The metric segment is checked against the known set, so a queue named `hour` or one
|
|
61
|
+
* ending in `:completed` still resolves correctly. Anything that doesn't fit returns null
|
|
62
|
+
* and is then reported but never deleted, so a stray key can't be destroyed by accident.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseHistoryKey(key: string, namespace: string): ParsedKey | null;
|
|
65
|
+
/**
|
|
66
|
+
* Inspection and cleanup for the Redis keys written by `MetricsRecorder`.
|
|
67
|
+
*
|
|
68
|
+
* Every operation is confined to the recorder's namespace and driven by SCAN, so it never
|
|
69
|
+
* blocks Redis and never touches BullMQ's own keys. Deletes use UNLINK.
|
|
70
|
+
*/
|
|
71
|
+
export declare class MetricsHistoryAdmin {
|
|
72
|
+
private readonly redis;
|
|
73
|
+
private readonly keys;
|
|
74
|
+
private readonly ownsRedis;
|
|
75
|
+
constructor(opts: MetricsHistoryAdminOptions);
|
|
76
|
+
disconnect(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Per-queue footprint of the stored history.
|
|
79
|
+
*
|
|
80
|
+
* Every key has to be measured individually, since only `MEMORY USAGE` knows what a hash
|
|
81
|
+
* really costs. Issuing those one at a time would mean a round trip per key, which at a
|
|
82
|
+
* 90-day retention across a dozen queues runs into the thousands, so the measurements go
|
|
83
|
+
* out in pipelined batches instead. Still an ops-scale call rather than a hot path: it
|
|
84
|
+
* reads the whole namespace, so it belongs behind a debug endpoint, not a poll.
|
|
85
|
+
*/
|
|
86
|
+
stats(): Promise<HistoryStats>;
|
|
87
|
+
/**
|
|
88
|
+
* Size and entry count for each key, in pipelined batches so the cost is a handful of
|
|
89
|
+
* round trips rather than one per key. A key that expires between the scan and the
|
|
90
|
+
* measurement simply reads as zero rather than failing the whole call.
|
|
91
|
+
*/
|
|
92
|
+
private measure;
|
|
93
|
+
/**
|
|
94
|
+
* Deletes recorded history. Purging a single queue also subtracts that queue's minutes
|
|
95
|
+
* from the global rollup, so the cross-queue chart stays correct instead of keeping the
|
|
96
|
+
* removed queue's throughput folded into it forever.
|
|
97
|
+
*
|
|
98
|
+
* That correction covers the counter metrics only. The global runtime, waittime and
|
|
99
|
+
* queueage rollups keep the purged queue's contribution until their own retention drops
|
|
100
|
+
* it: a packed bucket vector cannot be decremented field by field, and a max gauge has no
|
|
101
|
+
* record of which queue produced the maximum, so there is nothing to subtract. The
|
|
102
|
+
* per-queue keys are still deleted either way. See SUMMABLE_METRICS.
|
|
103
|
+
*/
|
|
104
|
+
purge(opts?: PurgeOptions): Promise<PurgeResult>;
|
|
105
|
+
/**
|
|
106
|
+
* Removes one queue's buckets from the matching global key, so the cross-queue series
|
|
107
|
+
* reflects the queues that are left rather than keeping the removed queue folded in.
|
|
108
|
+
* Each tier is corrected from its own source key, because the tiers have independent
|
|
109
|
+
* retention and the minute hash may already be gone while the hourly one survives.
|
|
110
|
+
* Fields that drain to zero are dropped: the recorder never writes a zero bucket, so a
|
|
111
|
+
* leftover zero would read as recorded-but-idle instead of not recorded.
|
|
112
|
+
* Returns the number of global keys it deleted.
|
|
113
|
+
*
|
|
114
|
+
* Only the summable metrics are touched; the latency ones are skipped outright rather than
|
|
115
|
+
* silently producing a no-op subtraction of their packed values. See SUMMABLE_METRICS.
|
|
116
|
+
*/
|
|
117
|
+
private subtractDayFromGlobal;
|
|
118
|
+
/**
|
|
119
|
+
* Same idea for the daily rollup: the global totals hash is the sum of the per-queue
|
|
120
|
+
* totals hashes, so it is corrected from those rather than re-derived from day hashes,
|
|
121
|
+
* which may already have expired. Returns the number of global fields it removed.
|
|
122
|
+
*
|
|
123
|
+
* Skips the latency metrics for the same reason as subtractDayFromGlobal.
|
|
124
|
+
*/
|
|
125
|
+
private subtractTotalsFromGlobal;
|
|
126
|
+
/**
|
|
127
|
+
* SCAN over the namespace, once per master: SCAN carries no key, so a cluster client has
|
|
128
|
+
* no slot to route by and would answer from one arbitrary node. SCAN may also hand back the
|
|
129
|
+
* same key on more than one cursor iteration, which would double-count in `stats()`, so
|
|
130
|
+
* emissions are de-duped here. The set is bounded by queues x metrics x retention days.
|
|
131
|
+
*/
|
|
132
|
+
private scan;
|
|
133
|
+
}
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsHistoryAdmin = void 0;
|
|
4
|
+
exports.parseHistoryKey = parseHistoryKey;
|
|
5
|
+
const connection_1 = require("./connection");
|
|
6
|
+
const keys_1 = require("./keys");
|
|
7
|
+
const SCAN_COUNT = 500;
|
|
8
|
+
const BATCH = 256;
|
|
9
|
+
/** Keys per pipelined `stats()` batch. Each key costs two commands. */
|
|
10
|
+
const MEASURE_BATCH = 250;
|
|
11
|
+
const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
+
const METRICS = ['completed', 'failed', 'runtime', 'waittime', 'queueage'];
|
|
13
|
+
/**
|
|
14
|
+
* Metrics whose global rollup is a plain summable counter, so one queue's share can be taken
|
|
15
|
+
* back out of it with HINCRBY. The latency metrics are deliberately absent: runtime and
|
|
16
|
+
* waittime pack a whole bucket vector into a single field and queueage holds a max gauge,
|
|
17
|
+
* and neither can be corrected by a scalar decrement. See `purge`.
|
|
18
|
+
*/
|
|
19
|
+
const SUMMABLE_METRICS = ['completed', 'failed'];
|
|
20
|
+
/**
|
|
21
|
+
* Parses the three key shapes from the right, because queue names may themselves contain
|
|
22
|
+
* colons:
|
|
23
|
+
*
|
|
24
|
+
* <ns>:<queue>:<metric>:<day> minute buckets
|
|
25
|
+
* <ns>:<queue>:<metric>:hour:<day> hourly rollup
|
|
26
|
+
* <ns>:<queue>:<metric>:totals daily totals
|
|
27
|
+
*
|
|
28
|
+
* The metric segment is checked against the known set, so a queue named `hour` or one
|
|
29
|
+
* ending in `:completed` still resolves correctly. Anything that doesn't fit returns null
|
|
30
|
+
* and is then reported but never deleted, so a stray key can't be destroyed by accident.
|
|
31
|
+
*/
|
|
32
|
+
function parseHistoryKey(key, namespace) {
|
|
33
|
+
const prefix = `${namespace}:`;
|
|
34
|
+
if (!key.startsWith(prefix)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const parts = key.slice(prefix.length).split(':');
|
|
38
|
+
if (parts.length < 3) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const last = parts[parts.length - 1];
|
|
42
|
+
if (last === 'totals' && METRICS.includes(parts[parts.length - 2])) {
|
|
43
|
+
const queue = parts.slice(0, -2).join(':');
|
|
44
|
+
return queue ? { queue, metric: parts[parts.length - 2], tier: 'day', day: null } : null;
|
|
45
|
+
}
|
|
46
|
+
if (!DAY_PATTERN.test(last)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (parts.length >= 4 && parts[parts.length - 2] === keys_1.HOUR_TIER) {
|
|
50
|
+
const metric = parts[parts.length - 3];
|
|
51
|
+
const queue = parts.slice(0, -3).join(':');
|
|
52
|
+
return queue && METRICS.includes(metric) ? { queue, metric, tier: 'hour', day: last } : null;
|
|
53
|
+
}
|
|
54
|
+
const metric = parts[parts.length - 2];
|
|
55
|
+
const queue = parts.slice(0, -2).join(':');
|
|
56
|
+
return queue && METRICS.includes(metric) ? { queue, metric, tier: 'minute', day: last } : null;
|
|
57
|
+
}
|
|
58
|
+
function emptyTiers() {
|
|
59
|
+
return {
|
|
60
|
+
minute: { keys: 0, bytes: 0 },
|
|
61
|
+
hour: { keys: 0, bytes: 0 },
|
|
62
|
+
day: { keys: 0, bytes: 0 },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The global rollup key mirroring a per-queue key, same tier and same day. */
|
|
66
|
+
function globalKeyFor(keys, parsed) {
|
|
67
|
+
if (parsed.day === null) {
|
|
68
|
+
return keys.totals(keys_1.GLOBAL_QUEUE, parsed.metric);
|
|
69
|
+
}
|
|
70
|
+
return parsed.tier === 'hour'
|
|
71
|
+
? keys.hour(keys_1.GLOBAL_QUEUE, parsed.metric, parsed.day)
|
|
72
|
+
: keys.day(keys_1.GLOBAL_QUEUE, parsed.metric, parsed.day);
|
|
73
|
+
}
|
|
74
|
+
function toDay(value) {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
if (!DAY_PATTERN.test(value)) {
|
|
77
|
+
throw new Error(`Expected a YYYY-MM-DD day or a Date, got "${value}"`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return value.toISOString().slice(0, 10);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Inspection and cleanup for the Redis keys written by `MetricsRecorder`.
|
|
85
|
+
*
|
|
86
|
+
* Every operation is confined to the recorder's namespace and driven by SCAN, so it never
|
|
87
|
+
* blocks Redis and never touches BullMQ's own keys. Deletes use UNLINK.
|
|
88
|
+
*/
|
|
89
|
+
class MetricsHistoryAdmin {
|
|
90
|
+
constructor(opts) {
|
|
91
|
+
const { client, owned } = (0, connection_1.resolveClient)(opts.connection);
|
|
92
|
+
this.redis = client;
|
|
93
|
+
this.ownsRedis = owned;
|
|
94
|
+
this.keys = (0, keys_1.metricsKeys)((0, keys_1.resolveNamespace)(opts.prefix, (0, connection_1.isCluster)(client)));
|
|
95
|
+
}
|
|
96
|
+
disconnect() {
|
|
97
|
+
if (this.ownsRedis) {
|
|
98
|
+
this.redis.disconnect();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Per-queue footprint of the stored history.
|
|
103
|
+
*
|
|
104
|
+
* Every key has to be measured individually, since only `MEMORY USAGE` knows what a hash
|
|
105
|
+
* really costs. Issuing those one at a time would mean a round trip per key, which at a
|
|
106
|
+
* 90-day retention across a dozen queues runs into the thousands, so the measurements go
|
|
107
|
+
* out in pipelined batches instead. Still an ops-scale call rather than a hot path: it
|
|
108
|
+
* reads the whole namespace, so it belongs behind a debug endpoint, not a poll.
|
|
109
|
+
*/
|
|
110
|
+
async stats() {
|
|
111
|
+
var _a;
|
|
112
|
+
const byQueue = new Map();
|
|
113
|
+
const tiers = emptyTiers();
|
|
114
|
+
let keys = 0;
|
|
115
|
+
let bytes = 0;
|
|
116
|
+
let minutes = 0;
|
|
117
|
+
let oldestDay = null;
|
|
118
|
+
let newestDay = null;
|
|
119
|
+
const found = [];
|
|
120
|
+
for await (const key of this.scan()) {
|
|
121
|
+
const parsed = parseHistoryKey(key, this.keys.namespace);
|
|
122
|
+
if (parsed) {
|
|
123
|
+
found.push({ key, parsed });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const measurements = await this.measure(found.map((item) => item.key));
|
|
127
|
+
for (const [index, { parsed }] of found.entries()) {
|
|
128
|
+
const { size, len } = measurements[index];
|
|
129
|
+
const entry = (_a = byQueue.get(parsed.queue)) !== null && _a !== void 0 ? _a : {
|
|
130
|
+
queue: parsed.queue,
|
|
131
|
+
keys: 0,
|
|
132
|
+
bytes: 0,
|
|
133
|
+
minutes: 0,
|
|
134
|
+
days: [],
|
|
135
|
+
tiers: emptyTiers(),
|
|
136
|
+
};
|
|
137
|
+
entry.keys += 1;
|
|
138
|
+
entry.bytes += size;
|
|
139
|
+
entry.tiers[parsed.tier].keys += 1;
|
|
140
|
+
entry.tiers[parsed.tier].bytes += size;
|
|
141
|
+
keys += 1;
|
|
142
|
+
bytes += size;
|
|
143
|
+
tiers[parsed.tier].keys += 1;
|
|
144
|
+
tiers[parsed.tier].bytes += size;
|
|
145
|
+
if (parsed.tier === 'minute') {
|
|
146
|
+
entry.minutes += len;
|
|
147
|
+
minutes += len;
|
|
148
|
+
}
|
|
149
|
+
if (parsed.day) {
|
|
150
|
+
entry.days.push(parsed.day);
|
|
151
|
+
if (oldestDay === null || parsed.day < oldestDay) {
|
|
152
|
+
oldestDay = parsed.day;
|
|
153
|
+
}
|
|
154
|
+
if (newestDay === null || parsed.day > newestDay) {
|
|
155
|
+
newestDay = parsed.day;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
byQueue.set(parsed.queue, entry);
|
|
159
|
+
}
|
|
160
|
+
const queues = [...byQueue.values()].sort((a, b) => b.bytes - a.bytes);
|
|
161
|
+
for (const queue of queues) {
|
|
162
|
+
queue.days = [...new Set(queue.days)].sort();
|
|
163
|
+
}
|
|
164
|
+
return { keys, bytes, minutes, oldestDay, newestDay, tiers, queues };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Size and entry count for each key, in pipelined batches so the cost is a handful of
|
|
168
|
+
* round trips rather than one per key. A key that expires between the scan and the
|
|
169
|
+
* measurement simply reads as zero rather than failing the whole call.
|
|
170
|
+
*/
|
|
171
|
+
async measure(keys) {
|
|
172
|
+
var _a, _b, _c, _d;
|
|
173
|
+
const out = [];
|
|
174
|
+
for (let i = 0; i < keys.length; i += MEASURE_BATCH) {
|
|
175
|
+
const chunk = keys.slice(i, i + MEASURE_BATCH);
|
|
176
|
+
const pipeline = this.redis.pipeline();
|
|
177
|
+
for (const key of chunk) {
|
|
178
|
+
pipeline.memory('USAGE', key);
|
|
179
|
+
pipeline.hlen(key);
|
|
180
|
+
}
|
|
181
|
+
const res = await pipeline.exec();
|
|
182
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
183
|
+
out.push({
|
|
184
|
+
size: Number((_b = (_a = res === null || res === void 0 ? void 0 : res[j * 2]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0) || 0,
|
|
185
|
+
len: Number((_d = (_c = res === null || res === void 0 ? void 0 : res[j * 2 + 1]) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 0) || 0,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Deletes recorded history. Purging a single queue also subtracts that queue's minutes
|
|
193
|
+
* from the global rollup, so the cross-queue chart stays correct instead of keeping the
|
|
194
|
+
* removed queue's throughput folded into it forever.
|
|
195
|
+
*
|
|
196
|
+
* That correction covers the counter metrics only. The global runtime, waittime and
|
|
197
|
+
* queueage rollups keep the purged queue's contribution until their own retention drops
|
|
198
|
+
* it: a packed bucket vector cannot be decremented field by field, and a max gauge has no
|
|
199
|
+
* record of which queue produced the maximum, so there is nothing to subtract. The
|
|
200
|
+
* per-queue keys are still deleted either way. See SUMMABLE_METRICS.
|
|
201
|
+
*/
|
|
202
|
+
async purge(opts = {}) {
|
|
203
|
+
const before = opts.before === undefined ? null : toDay(opts.before);
|
|
204
|
+
const result = { keysDeleted: 0, fieldsDeleted: 0 };
|
|
205
|
+
const dayKeys = [];
|
|
206
|
+
const totalsKeys = [];
|
|
207
|
+
for await (const key of this.scan()) {
|
|
208
|
+
const parsed = parseHistoryKey(key, this.keys.namespace);
|
|
209
|
+
if (!parsed) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (opts.queue !== undefined && parsed.queue !== opts.queue) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (parsed.day === null) {
|
|
216
|
+
totalsKeys.push({ key, parsed });
|
|
217
|
+
}
|
|
218
|
+
else if (before === null || parsed.day < before) {
|
|
219
|
+
dayKeys.push({ key, parsed });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Rewriting the global rollup only makes sense when a single queue is being removed:
|
|
223
|
+
// a full purge drops the global keys outright, along with everything else.
|
|
224
|
+
const adjustGlobal = opts.queue !== undefined && opts.queue !== keys_1.GLOBAL_QUEUE;
|
|
225
|
+
for (const { key, parsed } of dayKeys) {
|
|
226
|
+
if (adjustGlobal) {
|
|
227
|
+
result.keysDeleted += await this.subtractDayFromGlobal(key, parsed);
|
|
228
|
+
}
|
|
229
|
+
result.keysDeleted += await this.redis.unlink(key);
|
|
230
|
+
}
|
|
231
|
+
for (const { key, parsed } of totalsKeys) {
|
|
232
|
+
const stale = (await this.redis.hkeys(key)).filter((day) => before === null || day < before);
|
|
233
|
+
if (adjustGlobal && stale.length > 0) {
|
|
234
|
+
const totals = await this.redis.hmget(key, ...stale);
|
|
235
|
+
result.fieldsDeleted += await this.subtractTotalsFromGlobal(parsed.metric, stale, totals);
|
|
236
|
+
}
|
|
237
|
+
if (before === null) {
|
|
238
|
+
result.keysDeleted += await this.redis.unlink(key);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (let i = 0; i < stale.length; i += BATCH) {
|
|
242
|
+
result.fieldsDeleted += await this.redis.hdel(key, ...stale.slice(i, i + BATCH));
|
|
243
|
+
}
|
|
244
|
+
if ((await this.redis.hlen(key)) === 0) {
|
|
245
|
+
result.keysDeleted += await this.redis.unlink(key);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Removes one queue's buckets from the matching global key, so the cross-queue series
|
|
252
|
+
* reflects the queues that are left rather than keeping the removed queue folded in.
|
|
253
|
+
* Each tier is corrected from its own source key, because the tiers have independent
|
|
254
|
+
* retention and the minute hash may already be gone while the hourly one survives.
|
|
255
|
+
* Fields that drain to zero are dropped: the recorder never writes a zero bucket, so a
|
|
256
|
+
* leftover zero would read as recorded-but-idle instead of not recorded.
|
|
257
|
+
* Returns the number of global keys it deleted.
|
|
258
|
+
*
|
|
259
|
+
* Only the summable metrics are touched; the latency ones are skipped outright rather than
|
|
260
|
+
* silently producing a no-op subtraction of their packed values. See SUMMABLE_METRICS.
|
|
261
|
+
*/
|
|
262
|
+
async subtractDayFromGlobal(key, parsed) {
|
|
263
|
+
if (parsed.day === null || !SUMMABLE_METRICS.includes(parsed.metric)) {
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
const minutes = await this.redis.hgetall(key);
|
|
267
|
+
const globalDay = globalKeyFor(this.keys, parsed);
|
|
268
|
+
const pipeline = this.redis.multi();
|
|
269
|
+
const touched = [];
|
|
270
|
+
for (const field of Object.keys(minutes)) {
|
|
271
|
+
const value = Number(minutes[field]) || 0;
|
|
272
|
+
if (value === 0) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
touched.push(field);
|
|
276
|
+
pipeline.hincrby(globalDay, field, -value);
|
|
277
|
+
}
|
|
278
|
+
if (touched.length === 0) {
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
const res = await pipeline.exec();
|
|
282
|
+
const drained = touched.filter((_, i) => { var _a, _b; return Number((_b = (_a = res === null || res === void 0 ? void 0 : res[i]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0) <= 0; });
|
|
283
|
+
for (let i = 0; i < drained.length; i += BATCH) {
|
|
284
|
+
await this.redis.hdel(globalDay, ...drained.slice(i, i + BATCH));
|
|
285
|
+
}
|
|
286
|
+
return (await this.redis.hlen(globalDay)) === 0 ? await this.redis.unlink(globalDay) : 0;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Same idea for the daily rollup: the global totals hash is the sum of the per-queue
|
|
290
|
+
* totals hashes, so it is corrected from those rather than re-derived from day hashes,
|
|
291
|
+
* which may already have expired. Returns the number of global fields it removed.
|
|
292
|
+
*
|
|
293
|
+
* Skips the latency metrics for the same reason as subtractDayFromGlobal.
|
|
294
|
+
*/
|
|
295
|
+
async subtractTotalsFromGlobal(metric, days, values) {
|
|
296
|
+
if (!SUMMABLE_METRICS.includes(metric)) {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
const globalTotals = this.keys.totals(keys_1.GLOBAL_QUEUE, metric);
|
|
300
|
+
const pipeline = this.redis.multi();
|
|
301
|
+
const touched = [];
|
|
302
|
+
days.forEach((day, i) => {
|
|
303
|
+
const value = Number(values[i]) || 0;
|
|
304
|
+
if (value === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
touched.push(day);
|
|
308
|
+
pipeline.hincrby(globalTotals, day, -value);
|
|
309
|
+
});
|
|
310
|
+
if (touched.length === 0) {
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
const res = await pipeline.exec();
|
|
314
|
+
const drained = touched.filter((_, i) => { var _a, _b; return Number((_b = (_a = res === null || res === void 0 ? void 0 : res[i]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 0) <= 0; });
|
|
315
|
+
let removed = 0;
|
|
316
|
+
for (let i = 0; i < drained.length; i += BATCH) {
|
|
317
|
+
removed += await this.redis.hdel(globalTotals, ...drained.slice(i, i + BATCH));
|
|
318
|
+
}
|
|
319
|
+
return removed;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* SCAN over the namespace, once per master: SCAN carries no key, so a cluster client has
|
|
323
|
+
* no slot to route by and would answer from one arbitrary node. SCAN may also hand back the
|
|
324
|
+
* same key on more than one cursor iteration, which would double-count in `stats()`, so
|
|
325
|
+
* emissions are de-duped here. The set is bounded by queues x metrics x retention days.
|
|
326
|
+
*/
|
|
327
|
+
async *scan() {
|
|
328
|
+
const seen = new Set();
|
|
329
|
+
for (const target of (0, connection_1.scanTargets)(this.redis)) {
|
|
330
|
+
let cursor = '0';
|
|
331
|
+
do {
|
|
332
|
+
const [next, batch] = await target.scan(cursor, 'MATCH', this.keys.scanPattern, 'COUNT', SCAN_COUNT);
|
|
333
|
+
cursor = next;
|
|
334
|
+
for (const key of batch) {
|
|
335
|
+
if (!seen.has(key)) {
|
|
336
|
+
seen.add(key);
|
|
337
|
+
yield key;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} while (cursor !== '0');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
exports.MetricsHistoryAdmin = MetricsHistoryAdmin;
|
|
345
|
+
//# sourceMappingURL=HistoryAdmin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HistoryAdmin.js","sourceRoot":"","sources":["../src/HistoryAdmin.ts"],"names":[],"mappings":"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"}
|