@runsignal/node 0.1.0 → 0.2.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 +75 -45
- package/dist/cron.d.ts +33 -0
- package/dist/cron.js +33 -0
- package/dist/cron.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js +1 -1
- package/package.json +26 -8
package/README.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
# RunSignal for Node.js
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@runsignal/node)
|
|
4
|
+
[](https://www.npmjs.com/package/@runsignal/node)
|
|
5
|
+
[](https://github.com/gokhanertogan/RunSignal/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**Cron job and background job monitoring for Node.js, on OpenTelemetry.** Get an alert when a scheduled job
|
|
8
|
+
does not run, fails, runs too long or processes nothing, and when a BullMQ worker or sync loop stops making
|
|
9
|
+
progress: the failures no HTTP status code describes. Works with node-cron, croner, cron, node-schedule and
|
|
10
|
+
BullMQ, with no hosted service and no ping URLs.
|
|
11
|
+
|
|
12
|
+
RunSignal records runs, items and heartbeats as OpenTelemetry metrics, with the same names as every other
|
|
13
|
+
RunSignal SDK, so the `runsignal` CLI provisions the same alerts for Node.js services as for .NET, Go, Java and
|
|
14
|
+
Rust ones: New Relic alerts and dashboards, or Prometheus rules and a Grafana dashboard. The
|
|
15
|
+
[main README](https://github.com/gokhanertogan/RunSignal#readme) covers the concepts, the alerts and the
|
|
16
|
+
backends.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
7
19
|
|
|
8
20
|
```bash
|
|
9
|
-
npm install @runsignal/node @opentelemetry/api
|
|
21
|
+
npm install @runsignal/node @opentelemetry/api
|
|
10
22
|
```
|
|
11
23
|
|
|
12
|
-
Requires Node.js 20 or later. The package is ESM only
|
|
24
|
+
Requires Node.js 20 or later. The package is ESM only, ships its own TypeScript types, and depends only on
|
|
25
|
+
`@opentelemetry/api` (peer). BullMQ 5.16+ is an optional peer for the BullMQ integration.
|
|
13
26
|
|
|
14
|
-
##
|
|
27
|
+
## Quick start
|
|
15
28
|
|
|
16
29
|
```ts
|
|
17
30
|
import { defaultRegistry } from '@runsignal/node';
|
|
@@ -23,45 +36,55 @@ const invoices = defaultRegistry.workload('nightly-invoice', {
|
|
|
23
36
|
severity: 'critical',
|
|
24
37
|
});
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
await invoices.run(async (signal) => {
|
|
39
|
+
await invoices.run(async () => {
|
|
28
40
|
for (const order of await unbilledOrders()) {
|
|
29
|
-
await createInvoice(order
|
|
41
|
+
await createInvoice(order);
|
|
30
42
|
invoices.processed();
|
|
31
43
|
}
|
|
32
|
-
}
|
|
44
|
+
});
|
|
33
45
|
```
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
const sync = defaultRegistry.workload('stock-sync', { expectEvery: '5m' });
|
|
39
|
-
setInterval(async () => {
|
|
40
|
-
await syncStock();
|
|
41
|
-
sync.beat();
|
|
42
|
-
}, 60_000);
|
|
43
|
-
```
|
|
47
|
+
The registry follows the global meter provider, so it works even when it is created by an import before the
|
|
48
|
+
OpenTelemetry SDK starts. For an explicit provider, use `new WorkloadRegistry({ meterProvider })`.
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
## Recording work
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
run.succeed();
|
|
54
|
-
}
|
|
55
|
-
```
|
|
52
|
+
| Call | Records |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `w.run(fn, { signal })` | One run. It succeeds when `fn` resolves, is cancelled when `signal` was aborted, and fails otherwise. The run's span is active inside `fn` (with a context manager, which `NodeSDK` registers). |
|
|
55
|
+
| `const run = w.start()` then `run.succeed()` / `fail(err)` / `failCode(code, msg)` / `cancel()` | A run ended by hand. Later calls are ignored. |
|
|
56
|
+
| `w.processed(n)` / `w.itemFailed(n)` | Items handled. |
|
|
57
|
+
| `w.beat()` | A heartbeat from a long-running loop. |
|
|
56
58
|
|
|
57
59
|
`error.type` is the error's `errorType` property if it has one, then its `code` if that is a string (such as
|
|
58
60
|
`ECONNREFUSED`), then its class name.
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
before the OpenTelemetry SDK starts. Run spans are nested under the active span when a context manager is
|
|
62
|
-
registered, which `NodeSDK` does for you.
|
|
62
|
+
## Cron libraries
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Since 0.2.0, `cronJob` declares a scheduled job once, with its schedule, and gives you a tick handler for any cron library.
|
|
65
|
+
The workload is registered immediately, so a job that never fires still gets a `missed` alert.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { cronJob } from '@runsignal/node/cron';
|
|
69
|
+
|
|
70
|
+
const invoices = cronJob('nightly-invoice', { schedule: '0 2 * * *', maxDuration: '30m', minProcessed: 100 }, async (w) => {
|
|
71
|
+
for (const order of await unbilledOrders()) {
|
|
72
|
+
await createInvoice(order);
|
|
73
|
+
w.processed();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
cron.schedule(invoices.schedule, invoices.run); // node-cron
|
|
78
|
+
new Cron(invoices.schedule, invoices.run); // croner
|
|
79
|
+
CronJob.from({ cronTime: invoices.schedule, onTick: invoices.run, start: true }); // cron
|
|
80
|
+
schedule.scheduleJob(invoices.schedule, invoices.run); // node-schedule
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- **Failures:** a failed tick is recorded with its error type and passed to `onError` (default `console.error`). It is not rethrown, because most cron libraries turn a rejected tick into an unhandled rejection.
|
|
84
|
+
- **Options:** everything `workload()` takes, plus `registry` and `onError`. Declaring the same name twice throws.
|
|
85
|
+
- **Time zones:** if your scheduler runs the pattern in a time zone, pass the same one to the CLI with `--time-zone`.
|
|
86
|
+
|
|
87
|
+
## BullMQ
|
|
65
88
|
|
|
66
89
|
```ts
|
|
67
90
|
import { discoverSchedulers, instrument, workloadOf } from '@runsignal/node/bullmq';
|
|
@@ -75,20 +98,20 @@ await queue.upsertJobScheduler('Daily Sales Report', { pattern: '0 0 6 * * *' },
|
|
|
75
98
|
await discoverSchedulers(queue); // registers schedulers before they first fire
|
|
76
99
|
```
|
|
77
100
|
|
|
78
|
-
- **What
|
|
101
|
+
- **What is recorded:** every job, including success, failure with its error type, cancellation (when BullMQ aborts the job) and duration. Results and errors pass through, so retries work as before.
|
|
79
102
|
- **Naming:** jobs from a job scheduler are named after the scheduler (`Daily Sales Report` becomes `daily-sales-report`). Other jobs are named after their queue and job name (`emails.send-welcome`).
|
|
80
|
-
- **Discovery:** `discoverSchedulers` takes each scheduler's cron pattern as its schedule, so a scheduler that never fires gets a `missed` alert.
|
|
81
|
-
- **Redis client:** in ESM, BullMQ 6 needs a constructed
|
|
103
|
+
- **Discovery:** `discoverSchedulers` takes each scheduler's cron pattern as its schedule, so a scheduler that never fires gets a `missed` alert.
|
|
104
|
+
- **Redis client:** in ESM, BullMQ 6 needs a constructed client, such as `connection: new Redis({ maxRetriesPerRequest: null })` from `ioredis`.
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
## Manifest
|
|
84
107
|
|
|
85
108
|
```ts
|
|
86
109
|
defaultRegistry.writeManifestFromEnv('billing-service'); // writes to RUNSIGNAL_MANIFEST_PATH when it is set
|
|
87
110
|
```
|
|
88
111
|
|
|
89
|
-
|
|
112
|
+
## Exporting
|
|
90
113
|
|
|
91
|
-
|
|
114
|
+
**New Relic** needs delta temporality. With cumulative, a stopped workload never looks stopped:
|
|
92
115
|
|
|
93
116
|
```ts
|
|
94
117
|
new OTLPMetricExporter({
|
|
@@ -98,12 +121,15 @@ new OTLPMetricExporter({
|
|
|
98
121
|
});
|
|
99
122
|
```
|
|
100
123
|
|
|
101
|
-
|
|
124
|
+
**Prometheus 3** receives OTLP at `/api/v1/otlp/v1/metrics` with the default (cumulative) temporality. Generate
|
|
125
|
+
its rules with `runsignal export prometheus`.
|
|
126
|
+
|
|
127
|
+
A complete program is in [examples/worker](https://github.com/gokhanertogan/RunSignal/tree/main/src/RunSignal.Node/examples/worker).
|
|
102
128
|
|
|
103
129
|
## Performance
|
|
104
130
|
|
|
105
131
|
Measured on Apple Silicon with Node.js 26 and the OpenTelemetry metrics SDK attached; the times include its
|
|
106
|
-
aggregation. Run
|
|
132
|
+
aggregation. Run `npm run bench`.
|
|
107
133
|
|
|
108
134
|
| Operation | Time |
|
|
109
135
|
|---|---|
|
|
@@ -115,6 +141,10 @@ create the registry with `new WorkloadRegistry({ tracing: false })`.
|
|
|
115
141
|
|
|
116
142
|
## Development
|
|
117
143
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
`npm test` needs `redis-server` on the `PATH`; the BullMQ tests start their own instance.
|
|
145
|
+
|
|
146
|
+
## Links
|
|
147
|
+
|
|
148
|
+
[Specification](https://github.com/gokhanertogan/RunSignal/blob/main/spec/README.md) ·
|
|
149
|
+
[Changelog](https://github.com/gokhanertogan/RunSignal/blob/main/CHANGELOG.md) ·
|
|
150
|
+
[Other languages](https://github.com/gokhanertogan/RunSignal#quick-start)
|
package/dist/cron.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Workload, type WorkloadOptions, type WorkloadRegistry } from './registry.js';
|
|
2
|
+
export interface CronJobOptions extends WorkloadOptions {
|
|
3
|
+
/** Cron expression the job runs on. Pass `job.schedule` to your scheduler so the alert and the job agree. */
|
|
4
|
+
schedule: string;
|
|
5
|
+
/** The registry to record on. Defaults to `defaultRegistry`. */
|
|
6
|
+
registry?: WorkloadRegistry;
|
|
7
|
+
/**
|
|
8
|
+
* Called with the error of a failed run, after the failure is recorded. Defaults to `console.error`. The error
|
|
9
|
+
* is not rethrown, because most cron libraries turn a rejected tick into an unhandled rejection.
|
|
10
|
+
*/
|
|
11
|
+
onError?: (error: unknown, workload: Workload) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface CronJob {
|
|
14
|
+
/** The workload each tick runs as, for counting items or heartbeats outside `fn`. */
|
|
15
|
+
readonly workload: Workload;
|
|
16
|
+
/** The cron expression the job was declared with. */
|
|
17
|
+
readonly schedule: string;
|
|
18
|
+
/** Runs `fn` once as a run of the workload. Pass it to your cron library as the tick handler. */
|
|
19
|
+
readonly run: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Declares a scheduled job as a workload with `options.schedule` as its schedule, so it gets a missed alert from
|
|
23
|
+
* startup, even before it first fires. Works with any cron library: node-cron, croner, cron or node-schedule.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const invoices = cronJob('nightly-invoice', { schedule: '0 2 * * *', maxDuration: '30m' }, async (w) => {
|
|
27
|
+
* for (const order of await unbilledOrders()) { await createInvoice(order); w.processed(); }
|
|
28
|
+
* });
|
|
29
|
+
* cron.schedule(invoices.schedule, invoices.run); // node-cron
|
|
30
|
+
* new Cron(invoices.schedule, invoices.run); // croner
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function cronJob(name: string, options: CronJobOptions, fn: (workload: Workload) => unknown): CronJob;
|
package/dist/cron.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defaultRegistry } from './registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declares a scheduled job as a workload with `options.schedule` as its schedule, so it gets a missed alert from
|
|
4
|
+
* startup, even before it first fires. Works with any cron library: node-cron, croner, cron or node-schedule.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const invoices = cronJob('nightly-invoice', { schedule: '0 2 * * *', maxDuration: '30m' }, async (w) => {
|
|
8
|
+
* for (const order of await unbilledOrders()) { await createInvoice(order); w.processed(); }
|
|
9
|
+
* });
|
|
10
|
+
* cron.schedule(invoices.schedule, invoices.run); // node-cron
|
|
11
|
+
* new Cron(invoices.schedule, invoices.run); // croner
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function cronJob(name, options, fn) {
|
|
15
|
+
const { registry = defaultRegistry, onError = defaultOnError, ...expectations } = options;
|
|
16
|
+
const workload = registry.workload(name, expectations);
|
|
17
|
+
return {
|
|
18
|
+
workload,
|
|
19
|
+
schedule: options.schedule,
|
|
20
|
+
run: async () => {
|
|
21
|
+
try {
|
|
22
|
+
await workload.run(() => fn(workload));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
onError(error, workload);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function defaultOnError(error, workload) {
|
|
31
|
+
console.error(`runsignal: a run of ${workload.name} failed`, error);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=cron.js.map
|
package/dist/cron.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron.js","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA8D,MAAM,eAAe,CAAC;AAuB5G;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,OAAuB,EAAE,EAAmC;IAChG,MAAM,EAAE,QAAQ,GAAG,eAAe,EAAE,OAAO,GAAG,cAAc,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC;IAC1F,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACvD,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,GAAG,EAAE,KAAK,IAAI,EAAE;YACd,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAc,EAAE,QAAkB;IACxD,OAAO,CAAC,KAAK,CAAC,uBAAuB,QAAQ,CAAC,IAAI,SAAS,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SDK (see spec/README.md in the repository), so one set of alert rules covers every language.
|
|
4
4
|
*/
|
|
5
5
|
export declare const SCOPE_NAME = "RunSignal";
|
|
6
|
-
export declare const VERSION = "0.
|
|
6
|
+
export declare const VERSION = "0.2.0";
|
|
7
7
|
export declare const Metrics: {
|
|
8
8
|
readonly runs: 'runsignal.workload.runs';
|
|
9
9
|
readonly duration: 'runsignal.workload.duration';
|
package/dist/telemetry.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SDK (see spec/README.md in the repository), so one set of alert rules covers every language.
|
|
4
4
|
*/
|
|
5
5
|
export const SCOPE_NAME = 'RunSignal';
|
|
6
|
-
export const VERSION = '0.
|
|
6
|
+
export const VERSION = '0.2.0';
|
|
7
7
|
export const Metrics = {
|
|
8
8
|
runs: 'runsignal.workload.runs',
|
|
9
9
|
duration: 'runsignal.workload.duration',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runsignal/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Cron job and background job monitoring for Node.js with OpenTelemetry: alert when a scheduled job is missed, fails, overruns or stalls, or a BullMQ queue stops processing. Works with node-cron, croner, cron, node-schedule and BullMQ; alerts for New Relic, Prometheus and Grafana.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Gokhan Ertogan",
|
|
7
7
|
"repository": {
|
|
@@ -10,15 +10,29 @@
|
|
|
10
10
|
"directory": "src/RunSignal.Node"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
|
-
"
|
|
14
|
-
"opentelemetry",
|
|
15
|
-
"monitoring",
|
|
16
|
-
"alerting",
|
|
17
|
-
"background-jobs",
|
|
13
|
+
"cron-monitoring",
|
|
18
14
|
"cron",
|
|
15
|
+
"job-monitoring",
|
|
16
|
+
"background-jobs",
|
|
17
|
+
"scheduled-jobs",
|
|
19
18
|
"heartbeat",
|
|
19
|
+
"dead-mans-switch",
|
|
20
|
+
"healthcheck",
|
|
20
21
|
"bullmq",
|
|
21
|
-
"
|
|
22
|
+
"node-cron",
|
|
23
|
+
"croner",
|
|
24
|
+
"node-schedule",
|
|
25
|
+
"queue-monitoring",
|
|
26
|
+
"worker",
|
|
27
|
+
"opentelemetry",
|
|
28
|
+
"otel",
|
|
29
|
+
"observability",
|
|
30
|
+
"monitoring",
|
|
31
|
+
"alerting",
|
|
32
|
+
"prometheus",
|
|
33
|
+
"grafana",
|
|
34
|
+
"newrelic",
|
|
35
|
+
"typescript"
|
|
22
36
|
],
|
|
23
37
|
"type": "module",
|
|
24
38
|
"engines": {
|
|
@@ -32,6 +46,10 @@
|
|
|
32
46
|
"./bullmq": {
|
|
33
47
|
"types": "./dist/bullmq.d.ts",
|
|
34
48
|
"default": "./dist/bullmq.js"
|
|
49
|
+
},
|
|
50
|
+
"./cron": {
|
|
51
|
+
"types": "./dist/cron.d.ts",
|
|
52
|
+
"default": "./dist/cron.js"
|
|
35
53
|
}
|
|
36
54
|
},
|
|
37
55
|
"files": [
|