@rudderjs/horizon 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suleiman Shahbari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @rudderjs/horizon
2
+
3
+ Queue monitoring dashboard for RudderJS — tracks job lifecycle, queue metrics, and worker status with a built-in UI.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @rudderjs/horizon
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```ts
14
+ // bootstrap/providers.ts
15
+ import { horizon } from '@rudderjs/horizon'
16
+ import configs from '../config/index.js'
17
+ export default [..., horizon(configs.horizon), ...]
18
+ ```
19
+
20
+ ## Horizon Facade
21
+
22
+ ```ts
23
+ import { Horizon } from '@rudderjs/horizon'
24
+
25
+ const jobs = await Horizon.recentJobs({ queue: 'emails', perPage: 25 })
26
+ const failed = await Horizon.failedJobs()
27
+ const job = await Horizon.findJob('job-id')
28
+ const metrics = await Horizon.currentMetrics()
29
+ const workers = await Horizon.workers()
30
+ const count = await Horizon.jobCount('failed')
31
+ ```
32
+
33
+ ## `Horizon` Methods
34
+
35
+ | Method | Returns | Description |
36
+ |--------|---------|-------------|
37
+ | `recentJobs(options?)` | `HorizonJob[]` | List recent jobs with optional filters |
38
+ | `failedJobs(options?)` | `HorizonJob[]` | List failed jobs |
39
+ | `findJob(id)` | `HorizonJob \| null` | Find a single job by ID |
40
+ | `currentMetrics()` | `QueueMetric[]` | Latest metric snapshot per queue |
41
+ | `workers()` | `WorkerInfo[]` | All known workers and their status |
42
+ | `jobCount(status?)` | `number` | Count jobs, optionally by status |
43
+
44
+ ## Storage Drivers
45
+
46
+ - **`memory`** (default) — In-process, bounded by `maxJobs`. Good for development.
47
+ - **`sqlite`** — Persistent storage via `better-sqlite3`. Run `pnpm add better-sqlite3` to enable.
48
+
49
+ ## Configuration
50
+
51
+ ```ts
52
+ // config/horizon.ts
53
+ export default {
54
+ enabled: true,
55
+ path: 'horizon', // Dashboard route prefix
56
+ storage: 'memory', // 'memory' | 'sqlite'
57
+ sqlitePath: '.horizon.db',
58
+ maxJobs: 1000, // Max jobs in memory storage
59
+ pruneAfterHours: 72, // Auto-prune old records
60
+ metricsIntervalMs: 60_000, // Metrics polling interval
61
+ auth: null, // Optional auth callback for dashboard
62
+ } satisfies HorizonConfig
63
+ ```
64
+
65
+ ## Collectors
66
+
67
+ Horizon auto-registers three collectors on boot:
68
+
69
+ - **JobCollector** — Intercepts job dispatch/processing/completion/failure events
70
+ - **MetricsCollector** — Periodically polls queue adapter for throughput, wait time, runtime
71
+ - **WorkerCollector** — Tracks the current process as a worker (memory, job count)
72
+
73
+ ## Dashboard
74
+
75
+ Horizon serves a built-in UI at `/{path}` with pages for:
76
+
77
+ - Dashboard overview
78
+ - Recent jobs
79
+ - Failed jobs (with retry/delete)
80
+ - Queue metrics
81
+ - Worker status
82
+
83
+ ## Notes
84
+
85
+ - Requires `@rudderjs/queue` for job lifecycle hooks.
86
+ - Peers: `@rudderjs/router` and `@rudderjs/middleware` for route registration.
87
+ - Auto-prune runs on a background interval (does not block the event loop).
@@ -0,0 +1,38 @@
1
+ # @rudderjs/horizon — AI Coding Guidelines
2
+
3
+ ## What This Package Does
4
+
5
+ Horizon is a deep queue monitoring tool for RudderJS applications. It goes beyond basic job recording (Telescope) to provide full job lifecycle tracking, per-queue metrics, worker status, and failed job management (retry/delete).
6
+
7
+ ## Architecture
8
+
9
+ Three collectors:
10
+ 1. **JobCollector** — Wraps `QueueAdapter.dispatch()` to track full job lifecycle (pending → processing → completed/failed)
11
+ 2. **MetricsCollector** — Periodically snapshots per-queue stats: throughput, wait time, runtime, pending/active/completed/failed counts
12
+ 3. **WorkerCollector** — Reports current process as a worker with memory usage and job counts
13
+
14
+ ## Key Patterns
15
+
16
+ - `HorizonJob` records the full lifecycle: dispatchedAt, startedAt, completedAt, duration, exception
17
+ - Metrics are collected at configurable intervals (default 60s) and stored per-queue
18
+ - Worker status is self-reported from the current process
19
+ - Failed jobs can be retried via the API (delegates to `QueueAdapter.retryFailed()`)
20
+ - Uses `@rudderjs/queue` as a direct dependency (not optional)
21
+
22
+ ## API Endpoints
23
+
24
+ - `GET /horizon/api/stats` — Overview (job counts, queue metrics, worker count)
25
+ - `GET /horizon/api/jobs/recent` — Recent jobs with filtering
26
+ - `GET /horizon/api/jobs/failed` — Failed jobs list
27
+ - `GET /horizon/api/jobs/:id` — Job detail
28
+ - `POST /horizon/api/jobs/:id/retry` — Retry a failed job
29
+ - `DELETE /horizon/api/jobs/:id` — Delete a job record
30
+ - `GET /horizon/api/queues` — Current metrics for all queues
31
+ - `GET /horizon/api/queues/:queue` — 24h metric history for a specific queue
32
+ - `GET /horizon/api/workers` — Worker status list
33
+
34
+ ## Do NOT
35
+
36
+ - Block the dispatch path — storage writes should be fast (fire-and-forget for memory)
37
+ - Store full job payloads for sensitive data — use `safeSerialize` which strips functions
38
+ - Wrap the adapter if no adapter is registered — always check `QueueRegistry.get()` first
@@ -0,0 +1,6 @@
1
+ import type { HorizonStorage, HorizonConfig } from '../types.js';
2
+ /**
3
+ * Register all Horizon API + UI routes on the router.
4
+ */
5
+ export declare function registerRoutes(storage: HorizonStorage, config: HorizonConfig): Promise<void>;
6
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhE;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAG,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CAuIf"}
@@ -0,0 +1,140 @@
1
+ import { QueueRegistry } from '@rudderjs/queue';
2
+ import { dashboardPage, recentJobsPage, failedJobsPage, queuesPage, workersPage } from '../ui/pages.js';
3
+ /**
4
+ * Register all Horizon API + UI routes on the router.
5
+ */
6
+ export async function registerRoutes(storage, config) {
7
+ const { router } = await import('@rudderjs/router');
8
+ const basePath = `/${config.path ?? 'horizon'}`;
9
+ const prefix = `${basePath}/api`;
10
+ const middleware = config.auth ? [authMiddleware(config)] : [];
11
+ // ── UI Pages ─────────────────────────────────────────────
12
+ const html = (_req, res, content) => res.header('Content-Type', 'text/html').send(content);
13
+ router.get(basePath, (r, s) => html(r, s, dashboardPage(basePath, prefix)), middleware);
14
+ router.get(`${basePath}/jobs/recent`, (r, s) => html(r, s, recentJobsPage(basePath, prefix)), middleware);
15
+ router.get(`${basePath}/jobs/failed`, (r, s) => html(r, s, failedJobsPage(basePath, prefix)), middleware);
16
+ router.get(`${basePath}/queues`, (r, s) => html(r, s, queuesPage(basePath, prefix)), middleware);
17
+ router.get(`${basePath}/workers`, (r, s) => html(r, s, workersPage(basePath, prefix)), middleware);
18
+ // ── Overview stats ───────────────────────────────────────
19
+ router.get(`${prefix}/stats`, async (_req, res) => {
20
+ const [total, pending, processing, completed, failed, metrics, workerList] = await Promise.all([
21
+ storage.jobCount(),
22
+ storage.jobCount('pending'),
23
+ storage.jobCount('processing'),
24
+ storage.jobCount('completed'),
25
+ storage.jobCount('failed'),
26
+ storage.currentMetrics(),
27
+ storage.workers(),
28
+ ]);
29
+ res.json({
30
+ jobs: { total, pending, processing, completed, failed },
31
+ queues: metrics,
32
+ workers: workerList.length,
33
+ });
34
+ }, middleware);
35
+ // ── Recent jobs ──────────────────────────────────────────
36
+ router.get(`${prefix}/jobs/recent`, async (req, res) => {
37
+ const jobs = await storage.recentJobs({
38
+ page: parseInt(req.query['page'] ?? '1', 10),
39
+ perPage: parseInt(req.query['per_page'] ?? '50', 10),
40
+ queue: req.query['queue'],
41
+ search: req.query['search'],
42
+ status: req.query['status'],
43
+ });
44
+ const total = await storage.jobCount();
45
+ res.json({ data: jobs, meta: { total } });
46
+ }, middleware);
47
+ // ── Failed jobs ──────────────────────────────────────────
48
+ router.get(`${prefix}/jobs/failed`, async (req, res) => {
49
+ const jobs = await storage.failedJobs({
50
+ page: parseInt(req.query['page'] ?? '1', 10),
51
+ perPage: parseInt(req.query['per_page'] ?? '50', 10),
52
+ queue: req.query['queue'],
53
+ search: req.query['search'],
54
+ });
55
+ const total = await storage.jobCount('failed');
56
+ res.json({ data: jobs, meta: { total } });
57
+ }, middleware);
58
+ // ── Single job detail ────────────────────────────────────
59
+ router.get(`${prefix}/jobs/:id`, async (req, res) => {
60
+ const job = await storage.findJob(req.params['id'] ?? '');
61
+ if (!job) {
62
+ res.status(404).json({ message: 'Job not found.' });
63
+ return;
64
+ }
65
+ res.json({ data: job });
66
+ }, middleware);
67
+ // ── Retry a failed job ───────────────────────────────────
68
+ router.post(`${prefix}/jobs/:id/retry`, async (req, res) => {
69
+ const job = await storage.findJob(req.params['id'] ?? '');
70
+ if (!job) {
71
+ res.status(404).json({ message: 'Job not found.' });
72
+ return;
73
+ }
74
+ if (job.status !== 'failed') {
75
+ res.status(422).json({ message: 'Only failed jobs can be retried.' });
76
+ return;
77
+ }
78
+ // Use the queue adapter's retry if available, otherwise re-dispatch
79
+ const adapter = QueueRegistry.get();
80
+ if (adapter?.retryFailed) {
81
+ await adapter.retryFailed(job.queue);
82
+ storage.updateJob(job.id, { status: 'pending', exception: null });
83
+ res.json({ message: 'Job queued for retry.' });
84
+ }
85
+ else {
86
+ res.status(501).json({ message: 'Queue adapter does not support retry.' });
87
+ }
88
+ }, middleware);
89
+ // ── Delete a failed job ──────────────────────────────────
90
+ router.delete(`${prefix}/jobs/:id`, async (req, res) => {
91
+ const job = await storage.findJob(req.params['id'] ?? '');
92
+ if (!job) {
93
+ res.status(404).json({ message: 'Job not found.' });
94
+ return;
95
+ }
96
+ storage.deleteJob(job.id);
97
+ res.json({ message: 'Job deleted.' });
98
+ }, middleware);
99
+ // ── Queue-level metrics ──────────────────────────────────
100
+ router.get(`${prefix}/queues`, async (_req, res) => {
101
+ const metrics = await storage.currentMetrics();
102
+ res.json({ data: metrics });
103
+ }, middleware);
104
+ router.get(`${prefix}/queues/:queue`, async (req, res) => {
105
+ const queue = req.params['queue'] ?? 'default';
106
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1000); // last 24h
107
+ const history = await storage.metrics(queue, since);
108
+ // Also get live stats from the adapter if available
109
+ let live = null;
110
+ const adapter = QueueRegistry.get();
111
+ if (adapter?.status) {
112
+ try {
113
+ live = await adapter.status(queue);
114
+ }
115
+ catch {
116
+ // Not available
117
+ }
118
+ }
119
+ res.json({ queue, history, live });
120
+ }, middleware);
121
+ // ── Worker status ────────────────────────────────────────
122
+ router.get(`${prefix}/workers`, async (_req, res) => {
123
+ const workerList = await storage.workers();
124
+ res.json({ data: workerList });
125
+ }, middleware);
126
+ }
127
+ // ─── Auth Middleware ────────────────────────────────────────
128
+ function authMiddleware(config) {
129
+ return async (req, res, next) => {
130
+ if (config.auth) {
131
+ const allowed = await config.auth(req);
132
+ if (!allowed) {
133
+ res.status(403).json({ message: 'Unauthorized.' });
134
+ return;
135
+ }
136
+ }
137
+ return next();
138
+ };
139
+ }
140
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { HorizonStorage } from '../types.js';
2
+ /**
3
+ * Intercepts job dispatch and execution to record full job lifecycle.
4
+ * Wraps the QueueAdapter to capture dispatch, start, completion, and failure events.
5
+ */
6
+ export declare class JobCollector {
7
+ private readonly storage;
8
+ readonly name = "Job Collector";
9
+ constructor(storage: HorizonStorage);
10
+ register(): void;
11
+ }
12
+ //# sourceMappingURL=job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job.d.ts","sourceRoot":"","sources":["../../src/collectors/job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,aAAa,CAAA;AAE7D;;;GAGG;AACH,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,QAAQ,CAAC,IAAI,mBAAkB;gBAEF,OAAO,EAAE,cAAc;IAEpD,QAAQ,IAAI,IAAI;CAwEjB"}
@@ -0,0 +1,85 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { QueueRegistry } from '@rudderjs/queue';
3
+ /**
4
+ * Intercepts job dispatch and execution to record full job lifecycle.
5
+ * Wraps the QueueAdapter to capture dispatch, start, completion, and failure events.
6
+ */
7
+ export class JobCollector {
8
+ storage;
9
+ name = 'Job Collector';
10
+ constructor(storage) {
11
+ this.storage = storage;
12
+ }
13
+ register() {
14
+ const adapter = QueueRegistry.get();
15
+ if (!adapter)
16
+ return;
17
+ const storage = this.storage;
18
+ const originalDispatch = adapter.dispatch.bind(adapter);
19
+ adapter['dispatch'] = async (job, options) => {
20
+ const id = randomUUID();
21
+ const name = job.constructor.name;
22
+ const queue = options?.queue ?? job.constructor['queue'] ?? 'default';
23
+ const now = new Date();
24
+ // Record dispatch
25
+ const record = {
26
+ id,
27
+ name,
28
+ queue,
29
+ status: 'pending',
30
+ payload: safeSerialize(job),
31
+ attempts: 0,
32
+ exception: null,
33
+ dispatchedAt: now,
34
+ startedAt: null,
35
+ completedAt: null,
36
+ duration: null,
37
+ tags: [`job:${name}`, `queue:${queue}`],
38
+ };
39
+ storage.recordJob(record);
40
+ // Track start/complete/fail via wrapping the job's handle method
41
+ const originalHandle = job.handle.bind(job);
42
+ const originalFailed = job.failed?.bind(job);
43
+ job.handle = async () => {
44
+ const startedAt = new Date();
45
+ storage.updateJob(id, { status: 'processing', startedAt, attempts: record.attempts + 1 });
46
+ try {
47
+ await originalHandle();
48
+ const completedAt = new Date();
49
+ const duration = completedAt.getTime() - startedAt.getTime();
50
+ storage.updateJob(id, { status: 'completed', completedAt, duration });
51
+ }
52
+ catch (err) {
53
+ const completedAt = new Date();
54
+ const duration = completedAt.getTime() - startedAt.getTime();
55
+ storage.updateJob(id, {
56
+ status: 'failed',
57
+ completedAt,
58
+ duration,
59
+ exception: err instanceof Error ? err.message : String(err),
60
+ });
61
+ throw err;
62
+ }
63
+ };
64
+ if (originalFailed) {
65
+ job.failed = async (error) => {
66
+ storage.updateJob(id, {
67
+ status: 'failed',
68
+ exception: error instanceof Error ? error.message : String(error),
69
+ });
70
+ await originalFailed(error);
71
+ };
72
+ }
73
+ await originalDispatch(job, options);
74
+ };
75
+ }
76
+ }
77
+ function safeSerialize(obj) {
78
+ try {
79
+ return JSON.parse(JSON.stringify(obj));
80
+ }
81
+ catch {
82
+ return {};
83
+ }
84
+ }
85
+ //# sourceMappingURL=job.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job.js","sourceRoot":"","sources":["../../src/collectors/job.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { HorizonStorage } from '../types.js';
2
+ /**
3
+ * Periodically polls the queue adapter for stats and records metrics per queue.
4
+ * Also tracks job throughput by counting completed jobs in each interval.
5
+ */
6
+ export declare class MetricsCollector {
7
+ private readonly storage;
8
+ private readonly intervalMs;
9
+ readonly name = "Metrics Collector";
10
+ private throughputCounters;
11
+ private waitTimeAccum;
12
+ private runtimeAccum;
13
+ constructor(storage: HorizonStorage, intervalMs?: number);
14
+ register(): void;
15
+ /** Called by the job collector when a job completes */
16
+ recordJobCompleted(queue: string, waitTime: number, runtime: number): void;
17
+ private collect;
18
+ }
19
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/collectors/metrics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,aAAa,CAAA;AAE9D;;;GAGG;AACH,qBAAa,gBAAgB;IAOzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAP7B,QAAQ,CAAC,IAAI,uBAAsB;IACnC,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,OAAO,CAAC,aAAa,CAAyD;IAC9E,OAAO,CAAC,YAAY,CAAyD;gBAG1D,OAAO,EAAE,cAAc,EACvB,UAAU,GAAE,MAAe;IAG9C,QAAQ,IAAI,IAAI;IAKhB,uDAAuD;IACvD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;YAc5D,OAAO;CA2DtB"}
@@ -0,0 +1,90 @@
1
+ import { QueueRegistry } from '@rudderjs/queue';
2
+ /**
3
+ * Periodically polls the queue adapter for stats and records metrics per queue.
4
+ * Also tracks job throughput by counting completed jobs in each interval.
5
+ */
6
+ export class MetricsCollector {
7
+ storage;
8
+ intervalMs;
9
+ name = 'Metrics Collector';
10
+ throughputCounters = new Map();
11
+ waitTimeAccum = new Map();
12
+ runtimeAccum = new Map();
13
+ constructor(storage, intervalMs = 60_000) {
14
+ this.storage = storage;
15
+ this.intervalMs = intervalMs;
16
+ }
17
+ register() {
18
+ const timer = setInterval(() => this.collect(), this.intervalMs);
19
+ timer.unref();
20
+ }
21
+ /** Called by the job collector when a job completes */
22
+ recordJobCompleted(queue, waitTime, runtime) {
23
+ this.throughputCounters.set(queue, (this.throughputCounters.get(queue) ?? 0) + 1);
24
+ const wt = this.waitTimeAccum.get(queue) ?? { sum: 0, count: 0 };
25
+ wt.sum += waitTime;
26
+ wt.count += 1;
27
+ this.waitTimeAccum.set(queue, wt);
28
+ const rt = this.runtimeAccum.get(queue) ?? { sum: 0, count: 0 };
29
+ rt.sum += runtime;
30
+ rt.count += 1;
31
+ this.runtimeAccum.set(queue, rt);
32
+ }
33
+ async collect() {
34
+ const adapter = QueueRegistry.get();
35
+ if (!adapter)
36
+ return;
37
+ // Collect metrics for each queue we've seen throughput on
38
+ const queues = new Set(this.throughputCounters.keys());
39
+ // Also query the adapter for known queue stats
40
+ if (adapter.status) {
41
+ try {
42
+ const stats = await adapter.status();
43
+ // Default queue always exists
44
+ if (!queues.has('default'))
45
+ queues.add('default');
46
+ void stats; // We'll use per-queue stats below
47
+ }
48
+ catch {
49
+ // Adapter doesn't support per-queue stats
50
+ }
51
+ }
52
+ for (const queue of queues) {
53
+ const throughput = this.throughputCounters.get(queue) ?? 0;
54
+ const wt = this.waitTimeAccum.get(queue);
55
+ const rt = this.runtimeAccum.get(queue);
56
+ const avgWait = wt && wt.count > 0 ? Math.round((wt.sum / wt.count) * 100) / 100 : 0;
57
+ const avgRuntime = rt && rt.count > 0 ? Math.round((rt.sum / rt.count) * 100) / 100 : 0;
58
+ // Try to get real queue stats from the adapter
59
+ let pending = 0, active = 0, completed = 0, failed = 0;
60
+ if (adapter.status) {
61
+ try {
62
+ const stats = await adapter.status(queue);
63
+ pending = stats.waiting;
64
+ active = stats.active;
65
+ completed = stats.completed;
66
+ failed = stats.failed;
67
+ }
68
+ catch {
69
+ // Not supported for this queue
70
+ }
71
+ }
72
+ const metric = {
73
+ queue,
74
+ throughput,
75
+ waitTime: avgWait,
76
+ runtime: avgRuntime,
77
+ pending,
78
+ active,
79
+ completed,
80
+ failed,
81
+ };
82
+ this.storage.recordMetric(metric);
83
+ }
84
+ // Reset accumulators
85
+ this.throughputCounters.clear();
86
+ this.waitTimeAccum.clear();
87
+ this.runtimeAccum.clear();
88
+ }
89
+ }
90
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/collectors/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { HorizonStorage } from '../types.js';
2
+ /**
3
+ * Tracks the current process as a worker.
4
+ * Reports memory usage and job count periodically.
5
+ */
6
+ export declare class WorkerCollector {
7
+ private readonly storage;
8
+ private readonly queue;
9
+ readonly name = "Worker Collector";
10
+ private readonly workerId;
11
+ private jobsRun;
12
+ private lastJobAt;
13
+ constructor(storage: HorizonStorage, queue?: string);
14
+ register(): void;
15
+ /** Call when a job is processed by this worker */
16
+ recordJobProcessed(): void;
17
+ private report;
18
+ }
19
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/collectors/worker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,aAAa,CAAA;AAE7D;;;GAGG;AACH,qBAAa,eAAe;IAOxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,QAAQ,CAAC,IAAI,sBAAqB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,SAAS,CAAoB;gBAGlB,OAAO,EAAE,cAAc,EACvB,KAAK,GAAE,MAAkB;IAK5C,QAAQ,IAAI,IAAI;IAShB,kDAAkD;IAClD,kBAAkB,IAAI,IAAI;IAK1B,OAAO,CAAC,MAAM;CAaf"}
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Tracks the current process as a worker.
4
+ * Reports memory usage and job count periodically.
5
+ */
6
+ export class WorkerCollector {
7
+ storage;
8
+ queue;
9
+ name = 'Worker Collector';
10
+ workerId;
11
+ jobsRun = 0;
12
+ lastJobAt = null;
13
+ constructor(storage, queue = 'default') {
14
+ this.storage = storage;
15
+ this.queue = queue;
16
+ this.workerId = `worker-${randomUUID().slice(0, 8)}`;
17
+ }
18
+ register() {
19
+ // Report initial status
20
+ this.report('active');
21
+ // Periodically update
22
+ const timer = setInterval(() => this.report('active'), 30_000);
23
+ timer.unref();
24
+ }
25
+ /** Call when a job is processed by this worker */
26
+ recordJobProcessed() {
27
+ this.jobsRun++;
28
+ this.lastJobAt = new Date();
29
+ }
30
+ report(status) {
31
+ const memUsage = process.memoryUsage();
32
+ const info = {
33
+ id: this.workerId,
34
+ queue: this.queue,
35
+ status,
36
+ jobsRun: this.jobsRun,
37
+ memoryMb: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100,
38
+ startedAt: new Date(),
39
+ lastJobAt: this.lastJobAt,
40
+ };
41
+ this.storage.recordWorker(info);
42
+ }
43
+ }
44
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/collectors/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGxC;;;GAGG;AACH,MAAM,OAAO,eAAe;IAOP;IACA;IAPV,IAAI,GAAG,kBAAkB,CAAA;IACjB,QAAQ,CAAQ;IACzB,OAAO,GAAG,CAAC,CAAA;IACX,SAAS,GAAgB,IAAI,CAAA;IAErC,YACmB,OAAuB,EACvB,QAAgB,SAAS;QADzB,YAAO,GAAP,OAAO,CAAgB;QACvB,UAAK,GAAL,KAAK,CAAoB;QAE1C,IAAI,CAAC,QAAQ,GAAG,UAAU,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAA;IACtD,CAAC;IAED,QAAQ;QACN,wBAAwB;QACxB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAErB,sBAAsB;QACtB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;QAC9D,KAAK,CAAC,KAAK,EAAE,CAAA;IACf,CAAC;IAED,kDAAkD;IAClD,kBAAkB;QAChB,IAAI,CAAC,OAAO,EAAE,CAAA;QACd,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAA;IAC7B,CAAC;IAEO,MAAM,CAAC,MAA4B;QACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE,CAAA;QACtC,MAAM,IAAI,GAAe;YACvB,EAAE,EAAS,IAAI,CAAC,QAAQ;YACxB,KAAK,EAAM,IAAI,CAAC,KAAK;YACrB,MAAM;YACN,OAAO,EAAI,IAAI,CAAC,OAAO;YACvB,QAAQ,EAAG,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG;YACpE,SAAS,EAAE,IAAI,IAAI,EAAE;YACrB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAA;QACD,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;CACF"}