@bull-board/metrics 8.4.0 → 8.6.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.
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LatencyStore = exports.QUEUE_AGE_METRIC = void 0;
4
+ const histogram_1 = require("./histogram");
5
+ const keys_1 = require("./keys");
6
+ exports.QUEUE_AGE_METRIC = 'queueage';
7
+ const SECONDS_PER_DAY = 86400;
8
+ const MS_PER_HOUR = 3600000;
9
+ function ttl(days) {
10
+ return String(Math.max(1, Math.floor(days * SECONDS_PER_DAY)));
11
+ }
12
+ /**
13
+ * Merges one tick's bucket vector into the queue's hour hash, its day totals, and the
14
+ * global rollup of both, in a single round trip.
15
+ *
16
+ * Packing all 18 counts into one hash field rather than one field per bucket is what keeps
17
+ * this affordable: Redis hash entry overhead dwarfs a two-byte count, so the packed form
18
+ * costs roughly an eighth of the field-per-bucket form. The trade is that HINCRBY no longer
19
+ * applies, hence the read-modify-write here.
20
+ *
21
+ * Unlike the counter path there is no delta trick, because the caller supplies increments
22
+ * rather than absolute values. Two recorders writing the same tick would double-count, which
23
+ * is what LatencySampler's lease prevents.
24
+ *
25
+ * KEYS[1] queue hour hash ARGV[1] hour field ARGV[4] hour-tier ttl
26
+ * KEYS[2] queue totals hash ARGV[2] day field ARGV[5] day-tier ttl
27
+ * KEYS[3] global hour hash ARGV[3] packed vector ARGV[6] oldest day to keep
28
+ * KEYS[4] global totals hash ARGV[7] bucket count
29
+ */
30
+ const MERGE_VECTOR = `
31
+ local function merge(key, field, incoming, width)
32
+ local current = redis.call('HGET', key, field)
33
+ local out = {}
34
+ local i = 1
35
+ if current then
36
+ for value in string.gmatch(current, '([^,]+)') do
37
+ out[i] = tonumber(value) or 0
38
+ i = i + 1
39
+ end
40
+ end
41
+ while i <= width do
42
+ out[i] = 0
43
+ i = i + 1
44
+ end
45
+ local j = 1
46
+ for value in string.gmatch(incoming, '([^,]+)') do
47
+ out[j] = (out[j] or 0) + (tonumber(value) or 0)
48
+ j = j + 1
49
+ end
50
+ redis.call('HSET', key, field, table.concat(out, ','))
51
+ end
52
+
53
+ local function trim(key, cutoff)
54
+ local fields = redis.call('HKEYS', key)
55
+ local stale = {}
56
+ for i = 1, #fields do
57
+ if fields[i] < cutoff then
58
+ stale[#stale + 1] = fields[i]
59
+ if #stale == 256 then
60
+ redis.call('HDEL', key, unpack(stale))
61
+ stale = {}
62
+ end
63
+ end
64
+ end
65
+ if #stale > 0 then
66
+ redis.call('HDEL', key, unpack(stale))
67
+ end
68
+ end
69
+
70
+ local width = tonumber(ARGV[7])
71
+ local newDay = redis.call('HEXISTS', KEYS[2], ARGV[2]) == 0
72
+
73
+ merge(KEYS[1], ARGV[1], ARGV[3], width)
74
+ merge(KEYS[2], ARGV[2], ARGV[3], width)
75
+ merge(KEYS[3], ARGV[1], ARGV[3], width)
76
+ merge(KEYS[4], ARGV[2], ARGV[3], width)
77
+
78
+ redis.call('EXPIRE', KEYS[1], ARGV[4])
79
+ redis.call('EXPIRE', KEYS[2], ARGV[5])
80
+ redis.call('EXPIRE', KEYS[3], ARGV[4])
81
+ redis.call('EXPIRE', KEYS[4], ARGV[5])
82
+
83
+ if newDay then
84
+ trim(KEYS[2], ARGV[6])
85
+ trim(KEYS[4], ARGV[6])
86
+ end
87
+ return 1
88
+ `;
89
+ /**
90
+ * Queue age is a gauge, so an hour holds the worst backlog seen in it and a day holds the
91
+ * worst of its hours. Summing would be meaningless -- which is also why the global rollup
92
+ * here is a MAX across queues rather than the four-key SUM MERGE_VECTOR does: "the oldest
93
+ * job waiting anywhere on this board" is the meaningful cross-queue number, adding queues'
94
+ * ages together would not be.
95
+ *
96
+ * Because it's "set if greater", a bucket's value never goes down within its own hour or
97
+ * day once a high value lands -- that's the correct meaning of "the worst backlog seen in
98
+ * this hour/day", both for a single queue and for the global rollup. A later, healthier
99
+ * queue doesn't erase an earlier spike from the same bucket; it only wins the buckets it
100
+ * writes to itself.
101
+ *
102
+ * The totals hashes need the same day cutoff MERGE_VECTOR applies: their TTL is refreshed on
103
+ * every tick, so without a trim they would accumulate one field per day for as long as the
104
+ * queue exists rather than for the retention window.
105
+ *
106
+ * KEYS[1] queue hour hash ARGV[1] hour field ARGV[4] hour-tier ttl
107
+ * KEYS[2] queue totals hash ARGV[2] day field ARGV[5] day-tier ttl
108
+ * KEYS[3] global hour hash ARGV[3] value ARGV[6] oldest day to keep
109
+ * KEYS[4] global totals hash
110
+ */
111
+ const MAX_GAUGE = `
112
+ local function setMax(key, field, value)
113
+ local current = tonumber(redis.call('HGET', key, field) or '-1')
114
+ if value > current then
115
+ redis.call('HSET', key, field, value)
116
+ end
117
+ end
118
+
119
+ local function trim(key, cutoff)
120
+ local fields = redis.call('HKEYS', key)
121
+ local stale = {}
122
+ for i = 1, #fields do
123
+ if fields[i] < cutoff then
124
+ stale[#stale + 1] = fields[i]
125
+ if #stale == 256 then
126
+ redis.call('HDEL', key, unpack(stale))
127
+ stale = {}
128
+ end
129
+ end
130
+ end
131
+ if #stale > 0 then
132
+ redis.call('HDEL', key, unpack(stale))
133
+ end
134
+ end
135
+
136
+ local value = tonumber(ARGV[3])
137
+ local newDay = redis.call('HEXISTS', KEYS[2], ARGV[2]) == 0
138
+
139
+ setMax(KEYS[1], ARGV[1], value)
140
+ setMax(KEYS[2], ARGV[2], value)
141
+ setMax(KEYS[3], ARGV[1], value)
142
+ setMax(KEYS[4], ARGV[2], value)
143
+
144
+ redis.call('EXPIRE', KEYS[1], ARGV[4])
145
+ redis.call('EXPIRE', KEYS[2], ARGV[5])
146
+ redis.call('EXPIRE', KEYS[3], ARGV[4])
147
+ redis.call('EXPIRE', KEYS[4], ARGV[5])
148
+
149
+ if newDay then
150
+ trim(KEYS[2], ARGV[6])
151
+ trim(KEYS[4], ARGV[6])
152
+ end
153
+ return 1
154
+ `;
155
+ class LatencyStore {
156
+ constructor(opts) {
157
+ this.redis = opts.redis;
158
+ this.retention = opts.retention;
159
+ }
160
+ async addSamples(queue, metric, hour, vector) {
161
+ const day = (0, keys_1.minuteToDay)(hour * 60);
162
+ await this.redis.eval(MERGE_VECTOR, 4, (0, keys_1.hourHashKey)(queue, metric, day), (0, keys_1.totalsHashKey)(queue, metric), (0, keys_1.hourHashKey)(keys_1.GLOBAL_QUEUE, metric, day), (0, keys_1.totalsHashKey)(keys_1.GLOBAL_QUEUE, metric), String(hour), day, (0, histogram_1.packVector)(vector), ttl(this.retention.hours), ttl(this.retention.days), (0, keys_1.shiftDay)(day, -this.retention.days), String(histogram_1.BUCKET_COUNT));
163
+ }
164
+ async recordQueueAge(queue, hour, ms) {
165
+ const day = (0, keys_1.minuteToDay)(hour * 60);
166
+ await this.redis.eval(MAX_GAUGE, 4, (0, keys_1.hourHashKey)(queue, exports.QUEUE_AGE_METRIC, day), (0, keys_1.totalsHashKey)(queue, exports.QUEUE_AGE_METRIC), (0, keys_1.hourHashKey)(keys_1.GLOBAL_QUEUE, exports.QUEUE_AGE_METRIC, day), (0, keys_1.totalsHashKey)(keys_1.GLOBAL_QUEUE, exports.QUEUE_AGE_METRIC), String(hour), day, String(Math.max(0, Math.round(ms))), ttl(this.retention.hours), ttl(this.retention.days), (0, keys_1.shiftDay)(day, -this.retention.days));
167
+ }
168
+ async readRange(queue, metric, granularity, days) {
169
+ const out = {};
170
+ if (granularity === 'day') {
171
+ const raw = await this.redis.hgetall((0, keys_1.totalsHashKey)(queue, metric));
172
+ for (const day of days) {
173
+ if (raw[day] !== undefined) {
174
+ out[day] = (0, histogram_1.unpackVector)(raw[day]);
175
+ }
176
+ }
177
+ return out;
178
+ }
179
+ // Up to one key per retention day, so these go out together rather than as a serial
180
+ // chain of round trips, matching how the counter path reads its day hashes.
181
+ const perDay = await Promise.all(days.map((day) => this.redis.hgetall((0, keys_1.hourHashKey)(queue, metric, day))));
182
+ for (const raw of perDay) {
183
+ for (const field of Object.keys(raw)) {
184
+ out[field] = out[field]
185
+ ? (0, histogram_1.mergeVectors)(out[field], (0, histogram_1.unpackVector)(raw[field]))
186
+ : (0, histogram_1.unpackVector)(raw[field]);
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+ async readQueueAge(queue, granularity, days) {
192
+ var _a;
193
+ const out = {};
194
+ if (granularity === 'day') {
195
+ const raw = await this.redis.hgetall((0, keys_1.totalsHashKey)(queue, exports.QUEUE_AGE_METRIC));
196
+ for (const day of days) {
197
+ if (raw[day] !== undefined) {
198
+ out[day] = Number(raw[day]) || 0;
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ const perDay = await Promise.all(days.map((day) => this.redis.hgetall((0, keys_1.hourHashKey)(queue, exports.QUEUE_AGE_METRIC, day))));
204
+ for (const raw of perDay) {
205
+ for (const field of Object.keys(raw)) {
206
+ out[field] = Math.max((_a = out[field]) !== null && _a !== void 0 ? _a : 0, Number(raw[field]) || 0);
207
+ }
208
+ }
209
+ return out;
210
+ }
211
+ /** Epoch ms for an absolute hour index, for building response timestamps. */
212
+ static hourToMs(hour) {
213
+ return hour * MS_PER_HOUR;
214
+ }
215
+ }
216
+ exports.LatencyStore = LatencyStore;
217
+ //# sourceMappingURL=LatencyStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LatencyStore.js","sourceRoot":"","sources":["../src/LatencyStore.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,30 @@ export interface MetricsRecorderOptions {
19
19
  */
20
20
  retentionDays?: number;
21
21
  snapshotIntervalMs?: number;
22
+ /**
23
+ * Latency histograms and the queue-age gauge. On by default: the package exists to give
24
+ * boards without a metrics stack something useful, and an opt-in feature is one nobody
25
+ * finds. At default retention this costs roughly 250 to 300KB per queue under typical
26
+ * traffic, up to about 575KB in a pathological worst case, plus a one-off shared cost of
27
+ * roughly 224KB for the cross-queue rollup regardless of queue count.
28
+ */
29
+ latency?: boolean;
30
+ /** Above this many finished jobs in one tick, the sampler subsamples. */
31
+ maxLatencySamplesPerTick?: number;
32
+ /**
33
+ * Test-oriented escape hatch: overrides the sampler's default 5s safety margin (see
34
+ * `LatencySampler`'s `SAFETY_MARGIN_MS`), which otherwise excludes jobs that finished
35
+ * just before a scan. Lets a test read back a sample immediately instead of sleeping
36
+ * past the margin.
37
+ */
38
+ latencySafetyMarginMs?: number;
39
+ /**
40
+ * Notified whenever a latency tick fails. Latency errors are swallowed on purpose so a
41
+ * failing scan cannot take the counter snapshot with it, which also means a collector
42
+ * broken since startup looks the same as a board with no traffic. Default stays silent;
43
+ * wire this to your logger to tell an empty chart from a broken one.
44
+ */
45
+ onLatencyError?: (error: unknown, queueName: string) => void;
22
46
  }
23
47
  export declare function resolveRetention(opts: {
24
48
  retention?: Partial<Retention>;
@@ -33,6 +57,8 @@ export declare class MetricsRecorder {
33
57
  private readonly lastMinute;
34
58
  private timer;
35
59
  private running;
60
+ readonly latencyEnabled: boolean;
61
+ private readonly latencySampler;
36
62
  constructor(opts: MetricsRecorderOptions);
37
63
  get retention(): Retention;
38
64
  start(): void;
@@ -5,6 +5,8 @@ exports.resolveRetention = resolveRetention;
5
5
  const ioredis_1 = require("ioredis");
6
6
  const dataMapping_1 = require("./dataMapping");
7
7
  const HistoryStore_1 = require("./HistoryStore");
8
+ const LatencySampler_1 = require("./LatencySampler");
9
+ const LatencyStore_1 = require("./LatencyStore");
8
10
  const METRICS = ['completed', 'failed'];
9
11
  const MS_PER_MINUTE = 60000;
10
12
  const MINUTES_PER_DAY = 1440;
@@ -38,10 +40,24 @@ class MetricsRecorder {
38
40
  this.ownsRedis = false;
39
41
  }
40
42
  else {
41
- this.redis = new ioredis_1.Redis(opts.connection);
43
+ // Default to RESP2 (ioredis v6 enables RESP3 by default). Keeps exact v5 wire parity and
44
+ // support for Redis < 6.0, whose `HELLO 3` handshake fails. Spread order lets an explicit
45
+ // caller `protocol` win. Only on the options path -- an injected client's protocol is its own.
46
+ this.redis = new ioredis_1.Redis({ protocol: 2, ...opts.connection });
42
47
  this.ownsRedis = true;
43
48
  }
44
49
  this.store = new HistoryStore_1.HistoryStore({ redis: this.redis, retention: resolveRetention(opts) });
50
+ this.latencyEnabled = opts.latency !== false;
51
+ this.latencySampler = this.latencyEnabled
52
+ ? new LatencySampler_1.LatencySampler({
53
+ redis: this.redis,
54
+ store: new LatencyStore_1.LatencyStore({ redis: this.redis, retention: resolveRetention(opts) }),
55
+ tickMs: this.intervalMs,
56
+ maxSamplesPerTick: opts.maxLatencySamplesPerTick,
57
+ safetyMarginMs: opts.latencySafetyMarginMs,
58
+ onError: opts.onLatencyError,
59
+ })
60
+ : null;
45
61
  }
46
62
  get retention() {
47
63
  return this.store.retention;
@@ -79,6 +95,9 @@ class MetricsRecorder {
79
95
  for (const metric of METRICS) {
80
96
  await this.snapshotOne(adapter, name, metric);
81
97
  }
98
+ if (this.latencySampler) {
99
+ await this.latencySampler.sample(adapter);
100
+ }
82
101
  }
83
102
  }
84
103
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsRecorder.js","sourceRoot":"","sources":["../src/MetricsRecorder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MetricsRecorder.js","sourceRoot":"","sources":["../src/MetricsRecorder.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { MetricsHistoryPoint, MetricsHistoryProvider, MetricsHistoryQuery } from '@bull-board/api/typings/app';
1
+ import type { MetricsHistoryPoint, MetricsHistoryProvider, MetricsHistoryQuery, MetricsLatencyPoint, MetricsLatencyQuery } from '@bull-board/api/typings/app';
2
2
  import { Redis, type RedisOptions } from 'ioredis';
3
3
  import { type HistoryStats, type PurgeOptions, type PurgeResult } from './HistoryAdmin';
4
4
  import { type Retention } from './HistoryStore';
@@ -10,6 +10,7 @@ export interface RedisMetricsHistoryProviderOptions {
10
10
  }
11
11
  export declare class RedisMetricsHistoryProvider implements MetricsHistoryProvider {
12
12
  private readonly store;
13
+ private readonly latencyStore;
13
14
  private readonly admin;
14
15
  private readonly redis;
15
16
  private readonly ownsRedis;
@@ -21,4 +22,5 @@ export declare class RedisMetricsHistoryProvider implements MetricsHistoryProvid
21
22
  /** Backs the board's "clear history" action. See MetricsHistoryAdmin.purge. */
22
23
  purge(options?: PurgeOptions): Promise<PurgeResult>;
23
24
  getHistory(query: MetricsHistoryQuery): Promise<MetricsHistoryPoint[]>;
25
+ getLatency(query: MetricsLatencyQuery): Promise<MetricsLatencyPoint[]>;
24
26
  }
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RedisMetricsHistoryProvider = void 0;
4
4
  const ioredis_1 = require("ioredis");
5
+ const histogram_1 = require("./histogram");
5
6
  const HistoryAdmin_1 = require("./HistoryAdmin");
6
7
  const HistoryStore_1 = require("./HistoryStore");
7
8
  const keys_1 = require("./keys");
9
+ const LatencyStore_1 = require("./LatencyStore");
8
10
  const MetricsRecorder_1 = require("./MetricsRecorder");
9
11
  const MS_PER_HOUR = 3600000;
10
12
  class RedisMetricsHistoryProvider {
@@ -14,12 +16,16 @@ class RedisMetricsHistoryProvider {
14
16
  this.ownsRedis = false;
15
17
  }
16
18
  else {
17
- this.redis = new ioredis_1.Redis(opts.connection);
19
+ // Default to RESP2 (ioredis v6 enables RESP3 by default). Keeps exact v5 wire parity and
20
+ // support for Redis < 6.0, whose `HELLO 3` handshake fails. Spread order lets an explicit
21
+ // caller `protocol` win. Only on the options path -- an injected client's protocol is its own.
22
+ this.redis = new ioredis_1.Redis({ protocol: 2, ...opts.connection });
18
23
  this.ownsRedis = true;
19
24
  }
20
25
  const retention = (0, MetricsRecorder_1.resolveRetention)(opts);
21
26
  this.retentionDays = retention.days;
22
27
  this.store = new HistoryStore_1.HistoryStore({ redis: this.redis, retention });
28
+ this.latencyStore = new LatencyStore_1.LatencyStore({ redis: this.redis, retention });
23
29
  this.admin = new HistoryAdmin_1.MetricsHistoryAdmin({ connection: this.redis });
24
30
  }
25
31
  disconnect() {
@@ -43,6 +49,19 @@ class RedisMetricsHistoryProvider {
43
49
  const maxSpanMs = (this.retentionDays + 1) * 86400000;
44
50
  const from = Math.max(query.from, query.to - maxSpanMs);
45
51
  const days = (0, keys_1.dayRange)(from, query.to);
52
+ if (query.metric === 'queueage') {
53
+ const ages = await this.latencyStore.readQueueAge(queue, query.granularity, days);
54
+ // Day points are stamped at the day's start, so an intraday `from` would drop the day
55
+ // it falls in. Floored, exactly as the counter path below does it.
56
+ const lowerBound = query.granularity === 'day' ? dayFloor(query.from) : query.from;
57
+ return Object.keys(ages)
58
+ .map((key) => ({
59
+ ts: query.granularity === 'day' ? (0, keys_1.dayToStartMs)(key) : Number(key) * MS_PER_HOUR,
60
+ value: ages[key],
61
+ }))
62
+ .filter((p) => p.ts >= lowerBound && p.ts <= query.to)
63
+ .sort((a, b) => a.ts - b.ts);
64
+ }
46
65
  if (query.granularity === 'day') {
47
66
  const rawTotals = await this.store.readDailyTotalsRaw(queue, query.metric, days);
48
67
  // Empty history only when no day in range was ever recorded (all fields missing).
@@ -74,6 +93,58 @@ class RedisMetricsHistoryProvider {
74
93
  .map(([ts, value]) => ({ ts, value }))
75
94
  .sort((a, b) => a.ts - b.ts);
76
95
  }
96
+ async getLatency(query) {
97
+ var _a;
98
+ const queue = (_a = query.queue) !== null && _a !== void 0 ? _a : keys_1.GLOBAL_QUEUE;
99
+ const maxSpanMs = (this.retentionDays + 1) * 86400000;
100
+ const from = Math.max(query.from, query.to - maxSpanMs);
101
+ const days = (0, keys_1.dayRange)(from, query.to);
102
+ if (query.granularity === 'range') {
103
+ // Percentiles don't merge: averaging per-day p95s isn't the same number as the p95 of
104
+ // the whole range. Read the day tier's bucket vectors and merge them, then compute each
105
+ // requested percentile once from the summed vector.
106
+ const raw = await this.latencyStore.readRange(queue, query.metric, 'day', days);
107
+ let merged = (0, histogram_1.emptyVector)();
108
+ for (const day of days) {
109
+ const vector = raw[day];
110
+ if (vector) {
111
+ merged = (0, histogram_1.mergeVectors)(merged, vector);
112
+ }
113
+ }
114
+ const count = (0, histogram_1.vectorTotal)(merged);
115
+ if (count === 0) {
116
+ return [];
117
+ }
118
+ const values = {};
119
+ for (const p of query.percentiles) {
120
+ values[String(p)] = (0, histogram_1.quantile)(merged, p);
121
+ }
122
+ return [{ ts: query.from, count: Math.round(count), values }];
123
+ }
124
+ const raw = await this.latencyStore.readRange(queue, query.metric, query.granularity, days);
125
+ // Same day-start alignment as getHistory: comparing a day bucket against a raw `from`
126
+ // would drop the oldest day and leave this chart one bucket shorter than the throughput
127
+ // chart drawn for the same range.
128
+ const lowerBound = query.granularity === 'day' ? dayFloor(query.from) : query.from;
129
+ const points = [];
130
+ for (const key of Object.keys(raw)) {
131
+ const ts = query.granularity === 'day' ? (0, keys_1.dayToStartMs)(key) : Number(key) * MS_PER_HOUR;
132
+ if (ts < lowerBound || ts > query.to) {
133
+ continue;
134
+ }
135
+ const vector = raw[key];
136
+ const count = (0, histogram_1.vectorTotal)(vector);
137
+ if (count === 0) {
138
+ continue;
139
+ }
140
+ const values = {};
141
+ for (const p of query.percentiles) {
142
+ values[String(p)] = (0, histogram_1.quantile)(vector, p);
143
+ }
144
+ points.push({ ts, count: Math.round(count), values });
145
+ }
146
+ return points.sort((a, b) => a.ts - b.ts);
147
+ }
77
148
  }
78
149
  exports.RedisMetricsHistoryProvider = RedisMetricsHistoryProvider;
79
150
  function dayFloor(ms) {
@@ -1 +1 @@
1
- {"version":3,"file":"RedisMetricsHistoryProvider.js","sourceRoot":"","sources":["../src/RedisMetricsHistoryProvider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RedisMetricsHistoryProvider.js","sourceRoot":"","sources":["../src/RedisMetricsHistoryProvider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Fixed bucket boundaries in milliseconds, roughly 2-3x steps. Fixed rather than
3
+ * configurable because two ranges holding different layouts cannot be merged into one
4
+ * percentile without layout versioning, and the merge is what makes multi-day queries work.
5
+ *
6
+ * The tail past 10 minutes exists for wait time, where a stalled queue genuinely sits for
7
+ * hours. Without it p99 wait collapses to "over 10 minutes" exactly when it needs to be
8
+ * actionable.
9
+ */
10
+ export declare const BUCKET_BOUNDS: number[];
11
+ /** One bucket per bound, plus a final overflow bucket for anything past the last bound. */
12
+ export declare const BUCKET_COUNT: number;
13
+ export declare function bucketIndex(ms: number): number;
14
+ export declare function emptyVector(): number[];
15
+ export declare function packVector(vector: number[]): string;
16
+ export declare function unpackVector(raw: string | null): number[];
17
+ export declare function mergeVectors(a: number[], b: number[]): number[];
18
+ export declare function vectorTotal(vector: number[]): number;
19
+ /**
20
+ * Prometheus-style histogram_quantile: find the bucket holding the rank, then interpolate
21
+ * linearly across that bucket's span. The result is an estimate bounded by bucket width,
22
+ * never more precise than the layout allows.
23
+ *
24
+ * Samples in the overflow bucket have no upper bound to interpolate towards, so they report
25
+ * the last finite bound rather than inventing a ceiling.
26
+ */
27
+ export declare function quantile(vector: number[], p: number): number;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUCKET_COUNT = exports.BUCKET_BOUNDS = void 0;
4
+ exports.bucketIndex = bucketIndex;
5
+ exports.emptyVector = emptyVector;
6
+ exports.packVector = packVector;
7
+ exports.unpackVector = unpackVector;
8
+ exports.mergeVectors = mergeVectors;
9
+ exports.vectorTotal = vectorTotal;
10
+ exports.quantile = quantile;
11
+ /**
12
+ * Fixed bucket boundaries in milliseconds, roughly 2-3x steps. Fixed rather than
13
+ * configurable because two ranges holding different layouts cannot be merged into one
14
+ * percentile without layout versioning, and the merge is what makes multi-day queries work.
15
+ *
16
+ * The tail past 10 minutes exists for wait time, where a stalled queue genuinely sits for
17
+ * hours. Without it p99 wait collapses to "over 10 minutes" exactly when it needs to be
18
+ * actionable.
19
+ */
20
+ exports.BUCKET_BOUNDS = [
21
+ 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 300000, 600000,
22
+ 1800000, 3600000, 21600000,
23
+ ];
24
+ /** One bucket per bound, plus a final overflow bucket for anything past the last bound. */
25
+ exports.BUCKET_COUNT = exports.BUCKET_BOUNDS.length + 1;
26
+ function bucketIndex(ms) {
27
+ for (let i = 0; i < exports.BUCKET_BOUNDS.length; i++) {
28
+ if (ms <= exports.BUCKET_BOUNDS[i]) {
29
+ return i;
30
+ }
31
+ }
32
+ return exports.BUCKET_COUNT - 1;
33
+ }
34
+ function emptyVector() {
35
+ return new Array(exports.BUCKET_COUNT).fill(0);
36
+ }
37
+ function packVector(vector) {
38
+ return vector.join(',');
39
+ }
40
+ function unpackVector(raw) {
41
+ const out = emptyVector();
42
+ if (!raw) {
43
+ return out;
44
+ }
45
+ const parts = raw.split(',');
46
+ for (let i = 0; i < exports.BUCKET_COUNT && i < parts.length; i++) {
47
+ out[i] = Number(parts[i]) || 0;
48
+ }
49
+ return out;
50
+ }
51
+ function mergeVectors(a, b) {
52
+ var _a, _b;
53
+ const out = emptyVector();
54
+ for (let i = 0; i < exports.BUCKET_COUNT; i++) {
55
+ out[i] = ((_a = a[i]) !== null && _a !== void 0 ? _a : 0) + ((_b = b[i]) !== null && _b !== void 0 ? _b : 0);
56
+ }
57
+ return out;
58
+ }
59
+ function vectorTotal(vector) {
60
+ var _a;
61
+ let total = 0;
62
+ for (let i = 0; i < exports.BUCKET_COUNT; i++) {
63
+ total += (_a = vector[i]) !== null && _a !== void 0 ? _a : 0;
64
+ }
65
+ return total;
66
+ }
67
+ /**
68
+ * Prometheus-style histogram_quantile: find the bucket holding the rank, then interpolate
69
+ * linearly across that bucket's span. The result is an estimate bounded by bucket width,
70
+ * never more precise than the layout allows.
71
+ *
72
+ * Samples in the overflow bucket have no upper bound to interpolate towards, so they report
73
+ * the last finite bound rather than inventing a ceiling.
74
+ */
75
+ function quantile(vector, p) {
76
+ var _a;
77
+ const total = vectorTotal(vector);
78
+ if (total === 0) {
79
+ return 0;
80
+ }
81
+ const target = (p / 100) * total;
82
+ let cumulative = 0;
83
+ for (let i = 0; i < exports.BUCKET_COUNT; i++) {
84
+ const count = (_a = vector[i]) !== null && _a !== void 0 ? _a : 0;
85
+ if (count === 0) {
86
+ continue;
87
+ }
88
+ if (cumulative + count >= target) {
89
+ if (i === exports.BUCKET_COUNT - 1) {
90
+ return exports.BUCKET_BOUNDS[exports.BUCKET_BOUNDS.length - 1];
91
+ }
92
+ const lower = i === 0 ? 0 : exports.BUCKET_BOUNDS[i - 1];
93
+ const upper = exports.BUCKET_BOUNDS[i];
94
+ const withinBucket = (target - cumulative) / count;
95
+ return Math.round(lower + (upper - lower) * withinBucket);
96
+ }
97
+ cumulative += count;
98
+ }
99
+ return exports.BUCKET_BOUNDS[exports.BUCKET_BOUNDS.length - 1];
100
+ }
101
+ //# sourceMappingURL=histogram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"histogram.js","sourceRoot":"","sources":["../src/histogram.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,3 +4,8 @@ export { MetricsRecorder } from './MetricsRecorder';
4
4
  export type { MetricsRecorderOptions } from './MetricsRecorder';
5
5
  export { RedisMetricsHistoryProvider } from './RedisMetricsHistoryProvider';
6
6
  export type { RedisMetricsHistoryProviderOptions } from './RedisMetricsHistoryProvider';
7
+ export { LatencySampler } from './LatencySampler';
8
+ export type { LatencySamplerOptions } from './LatencySampler';
9
+ export { LatencyStore, QUEUE_AGE_METRIC } from './LatencyStore';
10
+ export type { LatencyMetric } from './LatencyStore';
11
+ export { BUCKET_BOUNDS, BUCKET_COUNT, quantile } from './histogram';