@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 +21 -0
- package/README.md +87 -0
- package/boost/guidelines.md +38 -0
- package/dist/api/routes.d.ts +6 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +140 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/collectors/job.d.ts +12 -0
- package/dist/collectors/job.d.ts.map +1 -0
- package/dist/collectors/job.js +85 -0
- package/dist/collectors/job.js.map +1 -0
- package/dist/collectors/metrics.d.ts +19 -0
- package/dist/collectors/metrics.d.ts.map +1 -0
- package/dist/collectors/metrics.js +90 -0
- package/dist/collectors/metrics.js.map +1 -0
- package/dist/collectors/worker.d.ts +19 -0
- package/dist/collectors/worker.d.ts.map +1 -0
- package/dist/collectors/worker.js +44 -0
- package/dist/collectors/worker.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +47 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +303 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/layout.d.ts +6 -0
- package/dist/ui/layout.d.ts.map +1 -0
- package/dist/ui/layout.js +55 -0
- package/dist/ui/layout.js.map +1 -0
- package/dist/ui/pages.d.ts +6 -0
- package/dist/ui/pages.d.ts.map +1 -0
- package/dist/ui/pages.js +295 -0
- package/dist/ui/pages.js.map +1 -0
- package/package.json +49 -0
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"}
|