@runsignal/node 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gokhan Ertogan
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 CHANGED
@@ -1,3 +1,120 @@
1
- # Temporary Holding Version
1
+ # RunSignal for Node.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Monitor Node.js background work that no HTTP status code describes, such as BullMQ queues, job schedulers and
4
+ sync loops, with OpenTelemetry. It emits the same signals and manifest as every other RunSignal SDK, so the
5
+ `runsignal` CLI provisions the same New Relic alerts for Node.js services as for .NET and Go ones. See the
6
+ [main README](../../README.md) for the concepts and the alerts.
7
+
8
+ ```bash
9
+ npm install @runsignal/node @opentelemetry/api # once published; until then, build from source
10
+ ```
11
+
12
+ Requires Node.js 20 or later. The package is ESM only and ships its own TypeScript types.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { defaultRegistry } from '@runsignal/node';
18
+
19
+ const invoices = defaultRegistry.workload('nightly-invoice', {
20
+ schedule: '0 2 * * *',
21
+ maxDuration: '30m', // or milliseconds
22
+ minProcessed: 100,
23
+ severity: 'critical',
24
+ });
25
+
26
+ // One run: succeeds when fn resolves, is cancelled if the signal was aborted, fails otherwise.
27
+ await invoices.run(async (signal) => {
28
+ for (const order of await unbilledOrders()) {
29
+ await createInvoice(order, { signal });
30
+ invoices.processed();
31
+ }
32
+ }, { signal: shutdown.signal });
33
+ ```
34
+
35
+ A long-running loop sends heartbeats, so a loop that is alive but stuck is noticed:
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
+ ```
44
+
45
+ Manual runs, and failures without an error:
46
+
47
+ ```ts
48
+ const run = imports.start();
49
+ const file = await fetchStatement();
50
+ if (!file) {
51
+ run.failCode('statement_missing', 'no bank statement today');
52
+ } else {
53
+ run.succeed();
54
+ }
55
+ ```
56
+
57
+ `error.type` is the error's `errorType` property if it has one, then its `code` if that is a string (such as
58
+ `ECONNREFUSED`), then its class name.
59
+
60
+ The registry follows the global meter provider. It works even when it is created, for example by an import,
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.
63
+
64
+ ### BullMQ
65
+
66
+ ```ts
67
+ import { discoverSchedulers, instrument, workloadOf } from '@runsignal/node/bullmq';
68
+
69
+ const worker = new Worker('reports', instrument(async (job) => {
70
+ const rows = await buildReport(job.data);
71
+ workloadOf(job)?.processed(rows.length);
72
+ }, { workloads: { 'daily-sales-report': { maxDuration: '10m', severity: 'critical' } } }), { connection });
73
+
74
+ await queue.upsertJobScheduler('Daily Sales Report', { pattern: '0 0 6 * * *' }, { name: 'build' });
75
+ await discoverSchedulers(queue); // registers schedulers before they first fire
76
+ ```
77
+
78
+ - **What gets 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
+ - **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. Schedulers that use `every` get no schedule.
81
+ - **Redis client:** in ESM, BullMQ 6 needs a constructed Redis client, such as `connection: new Redis({ maxRetriesPerRequest: null })` from `ioredis`, instead of connection options.
82
+
83
+ ### Manifest
84
+
85
+ ```ts
86
+ defaultRegistry.writeManifestFromEnv('billing-service'); // writes to RUNSIGNAL_MANIFEST_PATH when it is set
87
+ ```
88
+
89
+ ### Exporting to New Relic
90
+
91
+ Use delta temporality. With cumulative, a stopped workload never looks stopped:
92
+
93
+ ```ts
94
+ new OTLPMetricExporter({
95
+ url: 'https://otlp.eu01.nr-data.net:4317', // US: https://otlp.nr-data.net:4317
96
+ metadata, // a grpc Metadata with api-key = your license key
97
+ temporalityPreference: AggregationTemporality.DELTA,
98
+ });
99
+ ```
100
+
101
+ A complete program is in [examples/worker](examples/worker).
102
+
103
+ ## Performance
104
+
105
+ Measured on Apple Silicon with Node.js 26 and the OpenTelemetry metrics SDK attached; the times include its
106
+ aggregation. Run with `npm run bench`.
107
+
108
+ | Operation | Time |
109
+ |---|---|
110
+ | `start()` + `succeed()` without tracing | ~0.7 µs |
111
+ | `processed()` / `beat()` | ~0.2–0.3 µs |
112
+
113
+ With tracing, the cost depends on your span processor. For workloads that run thousands of times per second,
114
+ create the registry with `new WorkloadRegistry({ tracing: false })`.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ npm test # needs redis-server on the PATH; the BullMQ tests start their own instance
120
+ ```
@@ -0,0 +1,33 @@
1
+ import type { Job, Processor, Queue } from 'bullmq';
2
+ import { type Workload, type WorkloadOptions, type WorkloadRegistry } from './registry.js';
3
+ export interface InstrumentOptions {
4
+ /** The registry to record on. Defaults to `defaultRegistry`. */
5
+ registry?: WorkloadRegistry;
6
+ /** Expectations per workload name, applied when the workload is first created. */
7
+ workloads?: Record<string, WorkloadOptions>;
8
+ }
9
+ /**
10
+ * The workload a job runs as: its job scheduler ID for jobs created by `upsertJobScheduler` (so repeatable jobs
11
+ * keep one name across runs), otherwise the queue and job name, such as `emails.send-welcome`.
12
+ */
13
+ export declare function workloadNameOf(job: Pick<Job, 'name' | 'queueName' | 'repeatJobKey'>): string;
14
+ /**
15
+ * Wraps a BullMQ processor so every job is recorded as a run of its workload: success, failure with the job's
16
+ * error type, cancellation (when BullMQ aborts the job) and duration. The processor's result and errors pass
17
+ * through unchanged, so retries and failed-job handling work as before. Count items with
18
+ * `workloadOf(job)?.processed()`.
19
+ *
20
+ * ```ts
21
+ * new Worker('emails', instrument(async (job) => send(job.data)), { connection });
22
+ * ```
23
+ */
24
+ export declare function instrument<T, R, N extends string>(processor: Processor<T, R, N>, options?: InstrumentOptions): Processor<T, R, N>;
25
+ /** The workload the job runs as inside an `instrument`ed processor, for counting items. */
26
+ export declare function workloadOf(job: object): Workload | undefined;
27
+ /**
28
+ * Registers a workload for every job scheduler of `queue`, with its cron pattern as the schedule, so a
29
+ * repeatable job that never fires is still alerted on. Call it after `upsertJobScheduler` and before writing
30
+ * the manifest. Schedulers that use `every` instead of a cron pattern get no schedule. Time zones are not
31
+ * carried over: pass `--time-zone` to the CLI if your patterns use one.
32
+ */
33
+ export declare function discoverSchedulers(queue: Pick<Queue, 'getJobSchedulers'>, options?: InstrumentOptions): Promise<Workload[]>;
package/dist/bullmq.js ADDED
@@ -0,0 +1,57 @@
1
+ import { toWorkloadName } from './naming.js';
2
+ import { defaultRegistry } from './registry.js';
3
+ /**
4
+ * The workload a job runs as: its job scheduler ID for jobs created by `upsertJobScheduler` (so repeatable jobs
5
+ * keep one name across runs), otherwise the queue and job name, such as `emails.send-welcome`.
6
+ */
7
+ export function workloadNameOf(job) {
8
+ return toWorkloadName(job.repeatJobKey ?? `${job.queueName}.${job.name}`);
9
+ }
10
+ /**
11
+ * Wraps a BullMQ processor so every job is recorded as a run of its workload: success, failure with the job's
12
+ * error type, cancellation (when BullMQ aborts the job) and duration. The processor's result and errors pass
13
+ * through unchanged, so retries and failed-job handling work as before. Count items with
14
+ * `workloadOf(job)?.processed()`.
15
+ *
16
+ * ```ts
17
+ * new Worker('emails', instrument(async (job) => send(job.data)), { connection });
18
+ * ```
19
+ */
20
+ export function instrument(processor, options = {}) {
21
+ const registry = options.registry ?? defaultRegistry;
22
+ const cache = new Map();
23
+ return (job, token, signal) => {
24
+ const name = workloadNameOf(job);
25
+ let workload = cache.get(name);
26
+ if (!workload) {
27
+ workload = registry.getOrAdd(name, options.workloads?.[name]);
28
+ cache.set(name, workload);
29
+ }
30
+ jobWorkloads.set(job, workload);
31
+ return workload.run(() => processor(job, token, signal), signal === undefined ? {} : { signal });
32
+ };
33
+ }
34
+ const jobWorkloads = new WeakMap();
35
+ /** The workload the job runs as inside an `instrument`ed processor, for counting items. */
36
+ export function workloadOf(job) {
37
+ return jobWorkloads.get(job);
38
+ }
39
+ /**
40
+ * Registers a workload for every job scheduler of `queue`, with its cron pattern as the schedule, so a
41
+ * repeatable job that never fires is still alerted on. Call it after `upsertJobScheduler` and before writing
42
+ * the manifest. Schedulers that use `every` instead of a cron pattern get no schedule. Time zones are not
43
+ * carried over: pass `--time-zone` to the CLI if your patterns use one.
44
+ */
45
+ export async function discoverSchedulers(queue, options = {}) {
46
+ const registry = options.registry ?? defaultRegistry;
47
+ const found = [];
48
+ for (const scheduler of await queue.getJobSchedulers()) {
49
+ const name = toWorkloadName(scheduler.key);
50
+ found.push(registry.getOrAdd(name, {
51
+ ...(scheduler.pattern && { schedule: scheduler.pattern }),
52
+ ...options.workloads?.[name],
53
+ }));
54
+ }
55
+ return found;
56
+ }
57
+ //# sourceMappingURL=bullmq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq.js","sourceRoot":"","sources":["../src/bullmq.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,eAAe,EAA8D,MAAM,eAAe,CAAC;AAS5G;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAqD;IAClF,OAAO,cAAc,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAyB,SAA6B,EAAE,OAAO,GAAsB,EAAE;IAC/G,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACrD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE1C,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9D,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC5B,CAAC;QACD,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAChC,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACnG,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,IAAI,OAAO,EAAoB,CAAC;AAErD,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,KAAsC,EAAE,OAAO,GAAsB,EAAE;IAC9G,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACrD,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,KAAK,MAAM,SAAS,IAAI,MAAM,KAAK,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC3C,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;YACjC,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC;YACzD,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC;SAC7B,CAAC,CAAC,CAAC;IACN,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { defaultRegistry, Run, Workload, WorkloadConfigurationError, WorkloadRegistry, type Duration, type ErrorWithType, type Expectations, type RegistryOptions, type Severity, type WorkloadManifest, type WorkloadOptions, } from './registry.js';
2
+ export { InvalidWorkloadNameError, toWorkloadName } from './naming.js';
3
+ export { Attributes, DURATION_BUCKETS, Metrics, SCOPE_NAME, VERSION } from './telemetry.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { defaultRegistry, Run, Workload, WorkloadConfigurationError, WorkloadRegistry, } from './registry.js';
2
+ export { InvalidWorkloadNameError, toWorkloadName } from './naming.js';
3
+ export { Attributes, DURATION_BUCKETS, Metrics, SCOPE_NAME, VERSION } from './telemetry.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,GAAG,EACH,QAAQ,EACR,0BAA0B,EAC1B,gBAAgB,GAQjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Thrown for workload names that are not 1-128 lowercase letters, digits, '.', '_' or '-'. */
2
+ export declare class InvalidWorkloadNameError extends Error {
3
+ name: string;
4
+ }
5
+ export declare function validateName(name: string): void;
6
+ /**
7
+ * Turns an arbitrary job identifier, such as `InvoiceJobs.GenerateInvoices` or `Reports/Daily Sales`, into a
8
+ * valid workload name such as `invoice-jobs.generate-invoices` or `reports-daily-sales`. Every RunSignal SDK
9
+ * derives names the same way (spec/conformance/workload-names.json).
10
+ */
11
+ export declare function toWorkloadName(identifier: string): string;
package/dist/naming.js ADDED
@@ -0,0 +1,61 @@
1
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
2
+ /** Thrown for workload names that are not 1-128 lowercase letters, digits, '.', '_' or '-'. */
3
+ export class InvalidWorkloadNameError extends Error {
4
+ name = 'InvalidWorkloadNameError';
5
+ }
6
+ export function validateName(name) {
7
+ if (!NAME_PATTERN.test(name)) {
8
+ throw new InvalidWorkloadNameError(`Workload name '${name}' is invalid. Use 1-128 lowercase letters, digits, '.', '_' or '-', starting with a letter or digit.`);
9
+ }
10
+ }
11
+ // Letters that do not decompose into a base letter and a combining mark.
12
+ const TRANSLITERATIONS = {
13
+ ı: 'i', ß: 'ss', ø: 'o', Ø: 'O', æ: 'ae', Æ: 'Ae', đ: 'd', Đ: 'D', ł: 'l', Ł: 'L',
14
+ };
15
+ const isLower = (c) => c !== undefined && c >= 'a' && c <= 'z';
16
+ const isUpper = (c) => c !== undefined && c >= 'A' && c <= 'Z';
17
+ const isDigit = (c) => c !== undefined && c >= '0' && c <= '9';
18
+ /**
19
+ * Turns an arbitrary job identifier, such as `InvoiceJobs.GenerateInvoices` or `Reports/Daily Sales`, into a
20
+ * valid workload name such as `invoice-jobs.generate-invoices` or `reports-daily-sales`. Every RunSignal SDK
21
+ * derives names the same way (spec/conformance/workload-names.json).
22
+ */
23
+ export function toWorkloadName(identifier) {
24
+ if (identifier.trim() === '') {
25
+ throw new InvalidWorkloadNameError('The identifier is empty.');
26
+ }
27
+ // "e-Arşiv gönderimi" -> "e-Arsiv gonderimi", so names stay readable instead of losing letters.
28
+ const ascii = [...identifier.normalize('NFD').replace(/\p{Mn}/gu, '')]
29
+ .map((c) => TRANSLITERATIONS[c] ?? c)
30
+ .join('');
31
+ const chars = [...ascii];
32
+ let out = '';
33
+ chars.forEach((c, i) => {
34
+ if (isUpper(c)) {
35
+ // Break camel case: "GenerateInvoices" -> "generate-invoices", "HTTPSync" -> "http-sync".
36
+ const prev = chars[i - 1];
37
+ const next = chars[i + 1];
38
+ const last = out.at(-1);
39
+ if (out.length > 0 && last !== '-' && last !== '.' && last !== '_' &&
40
+ (isLower(prev) || isDigit(prev) || (isUpper(prev) && isLower(next)))) {
41
+ out += '-';
42
+ }
43
+ out += c.toLowerCase();
44
+ }
45
+ else if (isLower(c) || isDigit(c) || c === '.' || c === '_') {
46
+ out += c;
47
+ }
48
+ else if (out.length > 0 && out.at(-1) !== '-') {
49
+ out += '-';
50
+ }
51
+ });
52
+ let name = out.replace(/^[-._]+|[-._]+$/g, '');
53
+ if (name.length > 128) {
54
+ name = name.slice(0, 128).replace(/[-._]+$/, '');
55
+ }
56
+ if (name === '') {
57
+ throw new InvalidWorkloadNameError(`'${identifier}' contains no letters or digits to name a workload after.`);
58
+ }
59
+ return name;
60
+ }
61
+ //# sourceMappingURL=naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+ import { type Attributes as OtelAttributes, type Counter, type Histogram, type MeterProvider, type Span, type Tracer, type TracerProvider } from '@opentelemetry/api';
2
+ import { toWorkloadName } from './naming.js';
3
+ /** How urgent a workload's alerts are; it routes notifications. */
4
+ export type Severity = 'info' | 'warning' | 'critical';
5
+ /** A duration in milliseconds, or a string such as `'90s'`, `'30m'`, `'2h'` or `'1d'`. */
6
+ export type Duration = number | string;
7
+ /**
8
+ * What a workload is expected to do. These define the alerts RunSignal provisions and do not change how signals
9
+ * are recorded.
10
+ */
11
+ export interface WorkloadOptions {
12
+ /** Cron expression for when the workload should run. Produces the missed alert. */
13
+ schedule?: string;
14
+ /** Longest acceptable run. Produces the overrun alert. */
15
+ maxDuration?: Duration;
16
+ /** Longest acceptable gap between heartbeats. Produces the stalled alert. */
17
+ expectEvery?: Duration;
18
+ /** Fewest items a run should process. Produces the low-output alert. */
19
+ minProcessed?: number;
20
+ /** Highest acceptable share (0 to 1) of failed runs or items. Produces the failure-rate alert. */
21
+ failureRateThreshold?: number;
22
+ /** How urgent the workload's alerts are. Defaults to `'warning'`. */
23
+ severity?: Severity;
24
+ }
25
+ /** The validated expectations of a workload, with durations in milliseconds. */
26
+ export interface Expectations {
27
+ readonly schedule?: string;
28
+ readonly maxDurationMs?: number;
29
+ readonly expectEveryMs?: number;
30
+ readonly minProcessed?: number;
31
+ readonly failureRateThreshold?: number;
32
+ readonly severity: Severity;
33
+ }
34
+ export interface RegistryOptions {
35
+ /** Records metrics on this provider instead of the global one. */
36
+ meterProvider?: MeterProvider;
37
+ /** Records run spans on this provider instead of the global one. */
38
+ tracerProvider?: TracerProvider;
39
+ /** Set to false to record no span per run, for workloads that run thousands of times per second. */
40
+ tracing?: boolean;
41
+ }
42
+ /** An error can supply its own low-cardinality `error.type`, such as `'statement_missing'`. */
43
+ export interface ErrorWithType {
44
+ errorType: string;
45
+ }
46
+ /** Thrown when a workload's options are invalid or declared twice. */
47
+ export declare class WorkloadConfigurationError extends Error {
48
+ name: string;
49
+ }
50
+ /** @internal */
51
+ export interface Instruments {
52
+ readonly provider: MeterProvider;
53
+ readonly runs: Counter;
54
+ readonly duration: Histogram;
55
+ readonly items: Counter;
56
+ readonly heartbeats: Counter;
57
+ }
58
+ /** Creates and holds the workloads of an application and the instruments they record on. */
59
+ export declare class WorkloadRegistry {
60
+ #private;
61
+ /** @internal */ readonly tracer: Tracer | undefined;
62
+ /**
63
+ * By default the registry records on the global OpenTelemetry providers. It follows the global meter provider
64
+ * when it changes, so it works even when it is created, for example by an import, before the SDK starts.
65
+ */
66
+ constructor(options?: RegistryOptions);
67
+ /** @internal The instruments to record on, recreated if the global meter provider has changed. */
68
+ get instruments(): Instruments;
69
+ /**
70
+ * Returns the workload called `name`, creating it with `options` on first use. Call it once and keep the
71
+ * result. Passing options for a workload that already exists throws, so expectations are declared in exactly
72
+ * one place.
73
+ */
74
+ workload(name: string, options?: WorkloadOptions): Workload;
75
+ /**
76
+ * Returns the workload called `name`, creating it with `options` if it does not exist yet. Unlike `workload`,
77
+ * an existing workload is returned as is: options declared elsewhere take precedence. Scheduler integrations
78
+ * use it to name workloads after jobs.
79
+ */
80
+ getOrAdd(name: string, options?: WorkloadOptions): Workload;
81
+ /** Every workload created so far, sorted by name. */
82
+ get workloads(): Workload[];
83
+ /** Describes every workload, for alert provisioning. `service` must match the OpenTelemetry `service.name`. */
84
+ manifest(service: string): WorkloadManifest;
85
+ /** Writes the manifest as indented JSON, atomically, so a reader never sees half a manifest. */
86
+ writeManifest(path: string, service: string): void;
87
+ /**
88
+ * Writes the manifest if the RUNSIGNAL_MANIFEST_PATH environment variable is set, so CI can ask for one
89
+ * without code changes. Call it once every workload has been registered. The service name is `service`, or
90
+ * else OTEL_SERVICE_NAME, or else the script name. Returns the path written, or undefined.
91
+ */
92
+ writeManifestFromEnv(service?: string): string | undefined;
93
+ }
94
+ /** A workload manifest, in the format of spec/manifest.schema.json. */
95
+ export interface WorkloadManifest {
96
+ version: 1;
97
+ service: string;
98
+ workloads: Array<{
99
+ name: string;
100
+ schedule?: string;
101
+ maxDurationSeconds?: number;
102
+ expectEverySeconds?: number;
103
+ minProcessed?: number;
104
+ failureRateThreshold?: number;
105
+ severity: Severity;
106
+ }>;
107
+ }
108
+ type Outcome = 'success' | 'failure' | 'cancelled';
109
+ /**
110
+ * A unit of background work, such as a scheduled job, a queue consumer or a sync loop. Get one from a
111
+ * WorkloadRegistry once and keep it.
112
+ */
113
+ export declare class Workload {
114
+ #private;
115
+ readonly name: string;
116
+ readonly expectations: Expectations;
117
+ /** @internal */ readonly nameAttributes: OtelAttributes;
118
+ /** @internal */
119
+ constructor(registry: WorkloadRegistry, name: string, expectations: Expectations);
120
+ /**
121
+ * Starts a run. End it with exactly one call to `succeed`, `fail`, `failCode` or `cancel`; later calls are
122
+ * ignored. Prefer `run`, which ends the run for you and makes the run's span active for `fn`.
123
+ */
124
+ start(): Run;
125
+ /**
126
+ * Runs `fn` as one run of the workload: it succeeds when `fn` resolves, is cancelled when `fn` rejects after
127
+ * `signal` was aborted, and fails otherwise. The result or error of `fn` is passed through.
128
+ */
129
+ run<T>(fn: (signal?: AbortSignal) => T | Promise<T>, options?: {
130
+ signal?: AbortSignal;
131
+ }): Promise<T>;
132
+ /** Records `count` successfully processed items. Recording 0 is useful: it reports that a run did nothing. */
133
+ processed(count?: number): void;
134
+ /** Records `count` items that could not be processed. */
135
+ itemFailed(count?: number): void;
136
+ /** Records a heartbeat: the workload is alive and making progress. */
137
+ beat(): void;
138
+ /** @internal */
139
+ complete(start: number, span: Span | undefined, outcome: Outcome, errorType?: string, error?: unknown, description?: string): void;
140
+ /** @internal */
141
+ secondsSinceSuccess(now: number): number | undefined;
142
+ /** @internal */
143
+ secondsSinceHeartbeat(now: number): number | undefined;
144
+ }
145
+ /** One run of a Workload, started with `Workload.start`. */
146
+ export declare class Run {
147
+ #private;
148
+ readonly workload: Workload;
149
+ private readonly started;
150
+ readonly span: Span | undefined;
151
+ /** @internal */
152
+ constructor(workload: Workload, started: number, span: Span | undefined);
153
+ /** Milliseconds since the run started. */
154
+ get elapsedMs(): number;
155
+ /** Ends the run as successful. */
156
+ succeed(): void;
157
+ /**
158
+ * Ends the run as failed because of `error`. Its `error.type` is the error's `errorType` if it has one, then
159
+ * its `code` if that is a string (such as Node's `'ECONNREFUSED'`), then its class name.
160
+ */
161
+ fail(error: unknown): void;
162
+ /**
163
+ * Ends the run as failed without an error, for example when an expected input file is missing. `code` must be
164
+ * a short, stable value such as `'statement_missing'`; never put messages or IDs in it.
165
+ */
166
+ failCode(code: string, description?: string): void;
167
+ /** Ends the run as cancelled, for example on shutdown. Cancelled runs do not count as failures. */
168
+ cancel(): void;
169
+ }
170
+ /** A process-wide registry on the global OpenTelemetry providers. */
171
+ export declare const defaultRegistry: WorkloadRegistry;
172
+ export { toWorkloadName };
@@ -0,0 +1,353 @@
1
+ import { context, metrics, SpanKind, SpanStatusCode, trace, } from '@opentelemetry/api';
2
+ import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname } from 'node:path';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { toWorkloadName, validateName } from './naming.js';
6
+ import { Attributes, DURATION_BUCKETS, Metrics, SCOPE_NAME, VERSION } from './telemetry.js';
7
+ /** Thrown when a workload's options are invalid or declared twice. */
8
+ export class WorkloadConfigurationError extends Error {
9
+ name = 'WorkloadConfigurationError';
10
+ }
11
+ const UNIT_MS = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
12
+ function toMs(value, option) {
13
+ if (typeof value === 'number') {
14
+ if (!Number.isFinite(value) || value <= 0)
15
+ throw new WorkloadConfigurationError(`${option} must be a positive number of milliseconds.`);
16
+ return value;
17
+ }
18
+ const match = /^(\d+(?:\.\d+)?)([smhd])$/.exec(value.trim());
19
+ if (!match)
20
+ throw new WorkloadConfigurationError(`${option} '${value}' is not a duration. Use milliseconds or a number with s, m, h or d, such as '30m'.`);
21
+ return Number(match[1]) * UNIT_MS[match[2]];
22
+ }
23
+ function toExpectations(options) {
24
+ const { failureRateThreshold: rate, minProcessed } = options;
25
+ if (rate !== undefined && (rate < 0 || rate > 1))
26
+ throw new WorkloadConfigurationError(`failureRateThreshold must be between 0 and 1, got ${rate}.`);
27
+ if (minProcessed !== undefined && (!Number.isInteger(minProcessed) || minProcessed < 0))
28
+ throw new WorkloadConfigurationError('minProcessed must be a non-negative integer.');
29
+ const severity = options.severity ?? 'warning';
30
+ if (!['info', 'warning', 'critical'].includes(severity))
31
+ throw new WorkloadConfigurationError(`Unknown severity '${severity}'.`);
32
+ return {
33
+ severity,
34
+ ...(options.schedule !== undefined && { schedule: options.schedule }),
35
+ ...(options.maxDuration !== undefined && { maxDurationMs: toMs(options.maxDuration, 'maxDuration') }),
36
+ ...(options.expectEvery !== undefined && { expectEveryMs: toMs(options.expectEvery, 'expectEvery') }),
37
+ ...(minProcessed !== undefined && { minProcessed }),
38
+ ...(rate !== undefined && { failureRateThreshold: rate }),
39
+ };
40
+ }
41
+ /** Creates and holds the workloads of an application and the instruments they record on. */
42
+ export class WorkloadRegistry {
43
+ #workloads = new Map();
44
+ #meterProvider;
45
+ #instruments;
46
+ /** @internal */ tracer;
47
+ /**
48
+ * By default the registry records on the global OpenTelemetry providers. It follows the global meter provider
49
+ * when it changes, so it works even when it is created, for example by an import, before the SDK starts.
50
+ */
51
+ constructor(options = {}) {
52
+ this.#meterProvider = options.meterProvider;
53
+ this.#instruments = this.#createInstruments(options.meterProvider ?? metrics.getMeterProvider());
54
+ if (options.tracing !== false) {
55
+ // The global tracer provider is a proxy that follows the SDK, unlike the global meter provider.
56
+ this.tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(SCOPE_NAME, VERSION);
57
+ }
58
+ }
59
+ /** @internal The instruments to record on, recreated if the global meter provider has changed. */
60
+ get instruments() {
61
+ if (this.#meterProvider === undefined) {
62
+ const current = metrics.getMeterProvider();
63
+ if (current !== this.#instruments.provider)
64
+ this.#instruments = this.#createInstruments(current);
65
+ }
66
+ return this.#instruments;
67
+ }
68
+ #createInstruments(provider) {
69
+ const meter = provider.getMeter(SCOPE_NAME, VERSION);
70
+ const sinceSuccess = meter.createObservableGauge(Metrics.sinceSuccess, {
71
+ unit: 's',
72
+ description: 'Seconds since the last successful run of a workload, or since it was registered.',
73
+ });
74
+ const sinceHeartbeat = meter.createObservableGauge(Metrics.sinceHeartbeat, {
75
+ unit: 's',
76
+ description: 'Seconds since the last heartbeat of a workload, or since it was registered.',
77
+ });
78
+ meter.addBatchObservableCallback((observer) => {
79
+ const now = performance.now();
80
+ for (const workload of this.#workloads.values()) {
81
+ const success = workload.secondsSinceSuccess(now);
82
+ if (success !== undefined)
83
+ observer.observe(sinceSuccess, success, workload.nameAttributes);
84
+ const heartbeat = workload.secondsSinceHeartbeat(now);
85
+ if (heartbeat !== undefined)
86
+ observer.observe(sinceHeartbeat, heartbeat, workload.nameAttributes);
87
+ }
88
+ }, [sinceSuccess, sinceHeartbeat]);
89
+ return {
90
+ provider,
91
+ runs: meter.createCounter(Metrics.runs, { unit: '{run}', description: 'Completed runs of a workload.' }),
92
+ duration: meter.createHistogram(Metrics.duration, {
93
+ unit: 's',
94
+ description: 'Duration of workload runs.',
95
+ advice: { explicitBucketBoundaries: [...DURATION_BUCKETS] },
96
+ }),
97
+ items: meter.createCounter(Metrics.items, { unit: '{item}', description: 'Items handled by workload runs.' }),
98
+ heartbeats: meter.createCounter(Metrics.heartbeats, { unit: '{heartbeat}', description: 'Heartbeats of long-running workloads.' }),
99
+ };
100
+ }
101
+ /**
102
+ * Returns the workload called `name`, creating it with `options` on first use. Call it once and keep the
103
+ * result. Passing options for a workload that already exists throws, so expectations are declared in exactly
104
+ * one place.
105
+ */
106
+ workload(name, options) {
107
+ const existing = this.#workloads.get(name);
108
+ if (existing) {
109
+ if (options !== undefined) {
110
+ throw new WorkloadConfigurationError(`Workload '${name}' is already registered; set its options only where it is first created.`);
111
+ }
112
+ return existing;
113
+ }
114
+ return this.getOrAdd(name, options);
115
+ }
116
+ /**
117
+ * Returns the workload called `name`, creating it with `options` if it does not exist yet. Unlike `workload`,
118
+ * an existing workload is returned as is: options declared elsewhere take precedence. Scheduler integrations
119
+ * use it to name workloads after jobs.
120
+ */
121
+ getOrAdd(name, options = {}) {
122
+ const existing = this.#workloads.get(name);
123
+ if (existing)
124
+ return existing;
125
+ validateName(name);
126
+ const created = new Workload(this, name, toExpectations(options));
127
+ this.#workloads.set(name, created);
128
+ return created;
129
+ }
130
+ /** Every workload created so far, sorted by name. */
131
+ get workloads() {
132
+ return [...this.#workloads.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
133
+ }
134
+ /** Describes every workload, for alert provisioning. `service` must match the OpenTelemetry `service.name`. */
135
+ manifest(service) {
136
+ return {
137
+ version: 1,
138
+ service,
139
+ workloads: this.workloads.map(({ name, expectations: e }) => ({
140
+ name,
141
+ ...(e.schedule !== undefined && { schedule: e.schedule }),
142
+ ...(e.maxDurationMs !== undefined && { maxDurationSeconds: e.maxDurationMs / 1000 }),
143
+ ...(e.expectEveryMs !== undefined && { expectEverySeconds: e.expectEveryMs / 1000 }),
144
+ ...(e.minProcessed && { minProcessed: e.minProcessed }),
145
+ ...(e.failureRateThreshold && { failureRateThreshold: e.failureRateThreshold }),
146
+ severity: e.severity,
147
+ })),
148
+ };
149
+ }
150
+ /** Writes the manifest as indented JSON, atomically, so a reader never sees half a manifest. */
151
+ writeManifest(path, service) {
152
+ mkdirSync(dirname(path), { recursive: true });
153
+ writeFileSync(`${path}.tmp`, JSON.stringify(this.manifest(service), null, 2));
154
+ renameSync(`${path}.tmp`, path);
155
+ }
156
+ /**
157
+ * Writes the manifest if the RUNSIGNAL_MANIFEST_PATH environment variable is set, so CI can ask for one
158
+ * without code changes. Call it once every workload has been registered. The service name is `service`, or
159
+ * else OTEL_SERVICE_NAME, or else the script name. Returns the path written, or undefined.
160
+ */
161
+ writeManifestFromEnv(service) {
162
+ const path = process.env.RUNSIGNAL_MANIFEST_PATH?.trim();
163
+ if (!path)
164
+ return undefined;
165
+ this.writeManifest(path, service || process.env.OTEL_SERVICE_NAME || basename(process.argv[1] ?? 'node', '.js'));
166
+ return path;
167
+ }
168
+ }
169
+ /**
170
+ * A unit of background work, such as a scheduled job, a queue consumer or a sync loop. Get one from a
171
+ * WorkloadRegistry once and keep it.
172
+ */
173
+ export class Workload {
174
+ name;
175
+ expectations;
176
+ /** @internal */ nameAttributes;
177
+ #registry;
178
+ #outcomeAttributes;
179
+ #processed;
180
+ #failed;
181
+ // performance.now() timestamps. Until the first success or heartbeat they hold the time the workload was
182
+ // created, so a workload that never succeeds or never beats still reports a growing age.
183
+ #lastSuccess;
184
+ #lastHeartbeat;
185
+ #hasStarted = false;
186
+ #hasBeaten = false;
187
+ /** @internal */
188
+ constructor(registry, name, expectations) {
189
+ this.name = name;
190
+ this.expectations = expectations;
191
+ this.#registry = registry;
192
+ this.nameAttributes = Object.freeze({ [Attributes.workload]: name });
193
+ this.#outcomeAttributes = {
194
+ success: Object.freeze({ [Attributes.workload]: name, [Attributes.outcome]: 'success' }),
195
+ failure: Object.freeze({ [Attributes.workload]: name, [Attributes.outcome]: 'failure' }),
196
+ cancelled: Object.freeze({ [Attributes.workload]: name, [Attributes.outcome]: 'cancelled' }),
197
+ };
198
+ this.#processed = Object.freeze({ [Attributes.workload]: name, [Attributes.itemResult]: 'processed' });
199
+ this.#failed = Object.freeze({ [Attributes.workload]: name, [Attributes.itemResult]: 'failed' });
200
+ this.#lastSuccess = this.#lastHeartbeat = performance.now();
201
+ }
202
+ /**
203
+ * Starts a run. End it with exactly one call to `succeed`, `fail`, `failCode` or `cancel`; later calls are
204
+ * ignored. Prefer `run`, which ends the run for you and makes the run's span active for `fn`.
205
+ */
206
+ start() {
207
+ this.#hasStarted = true;
208
+ const span = this.#registry.tracer?.startSpan(this.name, { kind: SpanKind.INTERNAL, attributes: this.nameAttributes });
209
+ return new Run(this, performance.now(), span);
210
+ }
211
+ /**
212
+ * Runs `fn` as one run of the workload: it succeeds when `fn` resolves, is cancelled when `fn` rejects after
213
+ * `signal` was aborted, and fails otherwise. The result or error of `fn` is passed through.
214
+ */
215
+ async run(fn, options = {}) {
216
+ const run = this.start();
217
+ const active = run.span ? trace.setSpan(context.active(), run.span) : context.active();
218
+ try {
219
+ const result = await context.with(active, () => fn(options.signal));
220
+ run.succeed();
221
+ return result;
222
+ }
223
+ catch (error) {
224
+ if (options.signal?.aborted)
225
+ run.cancel();
226
+ else
227
+ run.fail(error);
228
+ throw error;
229
+ }
230
+ }
231
+ /** Records `count` successfully processed items. Recording 0 is useful: it reports that a run did nothing. */
232
+ processed(count = 1) {
233
+ if (count >= 0)
234
+ this.#registry.instruments.items.add(count, this.#processed);
235
+ }
236
+ /** Records `count` items that could not be processed. */
237
+ itemFailed(count = 1) {
238
+ if (count >= 0)
239
+ this.#registry.instruments.items.add(count, this.#failed);
240
+ }
241
+ /** Records a heartbeat: the workload is alive and making progress. */
242
+ beat() {
243
+ this.#lastHeartbeat = performance.now();
244
+ this.#hasBeaten = true;
245
+ this.#registry.instruments.heartbeats.add(1, this.nameAttributes);
246
+ }
247
+ /** @internal */
248
+ complete(start, span, outcome, errorType, error, description) {
249
+ const now = performance.now();
250
+ const instruments = this.#registry.instruments;
251
+ if (errorType === undefined) {
252
+ instruments.runs.add(1, this.#outcomeAttributes[outcome]);
253
+ }
254
+ else {
255
+ instruments.runs.add(1, { ...this.#outcomeAttributes.failure, [Attributes.errorType]: errorType });
256
+ }
257
+ instruments.duration.record((now - start) / 1000, this.#outcomeAttributes[outcome]);
258
+ if (outcome === 'success')
259
+ this.#lastSuccess = now;
260
+ if (span) {
261
+ if (span.isRecording()) {
262
+ span.setAttribute(Attributes.outcome, outcome);
263
+ if (outcome === 'failure') {
264
+ span.setAttribute(Attributes.errorType, errorType);
265
+ if (error instanceof Error)
266
+ span.recordException(error);
267
+ span.setStatus({ code: SpanStatusCode.ERROR, ...(description !== undefined && { message: description }) });
268
+ }
269
+ else if (outcome === 'success') {
270
+ span.setStatus({ code: SpanStatusCode.OK });
271
+ }
272
+ }
273
+ span.end();
274
+ }
275
+ }
276
+ // Reported for workloads that are scheduled or have run, so alerts can tell a missed run from a workload
277
+ // that simply has no runs.
278
+ /** @internal */
279
+ secondsSinceSuccess(now) {
280
+ return this.#hasStarted || this.expectations.schedule !== undefined ? (now - this.#lastSuccess) / 1000 : undefined;
281
+ }
282
+ // Reported for workloads that expect heartbeats or have sent one.
283
+ /** @internal */
284
+ secondsSinceHeartbeat(now) {
285
+ return this.#hasBeaten || this.expectations.expectEveryMs !== undefined ? (now - this.#lastHeartbeat) / 1000 : undefined;
286
+ }
287
+ }
288
+ /** One run of a Workload, started with `Workload.start`. */
289
+ export class Run {
290
+ workload;
291
+ started;
292
+ span;
293
+ #ended = false;
294
+ /** @internal */
295
+ constructor(workload, started, span) {
296
+ this.workload = workload;
297
+ this.started = started;
298
+ this.span = span;
299
+ }
300
+ /** Milliseconds since the run started. */
301
+ get elapsedMs() {
302
+ return performance.now() - this.started;
303
+ }
304
+ /** Ends the run as successful. */
305
+ succeed() {
306
+ if (this.#end())
307
+ this.workload.complete(this.started, this.span, 'success');
308
+ }
309
+ /**
310
+ * Ends the run as failed because of `error`. Its `error.type` is the error's `errorType` if it has one, then
311
+ * its `code` if that is a string (such as Node's `'ECONNREFUSED'`), then its class name.
312
+ */
313
+ fail(error) {
314
+ if (this.#end())
315
+ this.workload.complete(this.started, this.span, 'failure', errorTypeOf(error), error, error instanceof Error ? error.message : String(error));
316
+ }
317
+ /**
318
+ * Ends the run as failed without an error, for example when an expected input file is missing. `code` must be
319
+ * a short, stable value such as `'statement_missing'`; never put messages or IDs in it.
320
+ */
321
+ failCode(code, description) {
322
+ if (this.#end())
323
+ this.workload.complete(this.started, this.span, 'failure', code || 'unknown', undefined, description);
324
+ }
325
+ /** Ends the run as cancelled, for example on shutdown. Cancelled runs do not count as failures. */
326
+ cancel() {
327
+ if (this.#end())
328
+ this.workload.complete(this.started, this.span, 'cancelled');
329
+ }
330
+ #end() {
331
+ if (this.#ended)
332
+ return false;
333
+ this.#ended = true;
334
+ return true;
335
+ }
336
+ }
337
+ function errorTypeOf(error) {
338
+ if (typeof error === 'object' && error !== null) {
339
+ const { errorType, code } = error;
340
+ if (typeof errorType === 'string' && errorType)
341
+ return errorType;
342
+ if (typeof code === 'string' && code)
343
+ return code;
344
+ const name = error.constructor?.name;
345
+ if (name && name !== 'Object')
346
+ return name;
347
+ }
348
+ return typeof error;
349
+ }
350
+ /** A process-wide registry on the global OpenTelemetry providers. */
351
+ export const defaultRegistry = new WorkloadRegistry();
352
+ export { toWorkloadName };
353
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Names of the meter, tracer, instruments and attributes RunSignal emits. They are identical in every RunSignal
3
+ * SDK (see spec/README.md in the repository), so one set of alert rules covers every language.
4
+ */
5
+ export declare const SCOPE_NAME = "RunSignal";
6
+ export declare const VERSION = "0.1.0";
7
+ export declare const Metrics: {
8
+ readonly runs: 'runsignal.workload.runs';
9
+ readonly duration: 'runsignal.workload.duration';
10
+ readonly items: 'runsignal.workload.items';
11
+ readonly heartbeats: 'runsignal.workload.heartbeats';
12
+ readonly sinceSuccess: 'runsignal.workload.since_success';
13
+ readonly sinceHeartbeat: 'runsignal.workload.since_heartbeat';
14
+ };
15
+ export declare const Attributes: {
16
+ readonly workload: 'runsignal.workload';
17
+ readonly outcome: 'runsignal.outcome';
18
+ readonly itemResult: 'runsignal.item.result';
19
+ readonly errorType: 'error.type';
20
+ };
21
+ /**
22
+ * Histogram bucket boundaries, in seconds, for the duration metric: background work runs from milliseconds to
23
+ * hours, while the OpenTelemetry defaults stop at 10 seconds.
24
+ */
25
+ export declare const DURATION_BUCKETS: readonly number[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Names of the meter, tracer, instruments and attributes RunSignal emits. They are identical in every RunSignal
3
+ * SDK (see spec/README.md in the repository), so one set of alert rules covers every language.
4
+ */
5
+ export const SCOPE_NAME = 'RunSignal';
6
+ export const VERSION = '0.1.0';
7
+ export const Metrics = {
8
+ runs: 'runsignal.workload.runs',
9
+ duration: 'runsignal.workload.duration',
10
+ items: 'runsignal.workload.items',
11
+ heartbeats: 'runsignal.workload.heartbeats',
12
+ sinceSuccess: 'runsignal.workload.since_success',
13
+ sinceHeartbeat: 'runsignal.workload.since_heartbeat',
14
+ };
15
+ export const Attributes = {
16
+ workload: 'runsignal.workload',
17
+ outcome: 'runsignal.outcome',
18
+ itemResult: 'runsignal.item.result',
19
+ errorType: 'error.type',
20
+ };
21
+ /**
22
+ * Histogram bucket boundaries, in seconds, for the duration metric: background work runs from milliseconds to
23
+ * hours, while the OpenTelemetry defaults stop at 10 seconds.
24
+ */
25
+ export const DURATION_BUCKETS = [
26
+ 0.01, 0.05, 0.1, 0.5, 1, 5, 10, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 14400, 28800,
27
+ ];
28
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC;AACtC,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI,EAAE,yBAAyB;IAC/B,QAAQ,EAAE,6BAA6B;IACvC,KAAK,EAAE,0BAA0B;IACjC,UAAU,EAAE,+BAA+B;IAC3C,YAAY,EAAE,kCAAkC;IAChD,cAAc,EAAE,oCAAoC;CAC5C,CAAC;AAEX,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,QAAQ,EAAE,oBAAoB;IAC9B,OAAO,EAAE,mBAAmB;IAC5B,UAAU,EAAE,uBAAuB;IACnC,SAAS,EAAE,YAAY;CACf,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK;CAC3F,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@runsignal/node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Monitor Node.js background work that no HTTP status code describes (scheduled jobs, BullMQ queues, sync loops) with OpenTelemetry, and alert on missed, overrunning, stalled or low-output jobs.",
5
+ "license": "MIT",
6
+ "author": "Gokhan Ertogan",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/gokhanertogan/RunSignal.git",
10
+ "directory": "src/RunSignal.Node"
11
+ },
12
+ "keywords": [
13
+ "observability",
14
+ "opentelemetry",
15
+ "monitoring",
16
+ "alerting",
17
+ "background-jobs",
18
+ "cron",
19
+ "heartbeat",
20
+ "bullmq",
21
+ "newrelic"
22
+ ],
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./bullmq": {
33
+ "types": "./dist/bullmq.d.ts",
34
+ "default": "./dist/bullmq.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "test": "tsc -p tsconfig.test.json && node --test --test-concurrency=1 'dist-test/test/**/*.test.js'",
44
+ "bench": "tsc -p tsconfig.test.json && node dist-test/test/bench.js",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "@opentelemetry/api": "^1.9.0",
49
+ "bullmq": ">=5.16.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "bullmq": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@opentelemetry/api": "^1.9.0",
58
+ "@opentelemetry/context-async-hooks": "^2.12.0",
59
+ "@opentelemetry/sdk-metrics": "^2.12.0",
60
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
61
+ "@types/node": "^26.6.5",
62
+ "bullmq": "^6.4.0",
63
+ "ioredis": "^6.0.0",
64
+ "typescript": "^7.0.2"
65
+ },
66
+ "homepage": "https://github.com/gokhanertogan/RunSignal/tree/main/src/RunSignal.Node#readme",
67
+ "bugs": {
68
+ "url": "https://github.com/gokhanertogan/RunSignal/issues"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public",
72
+ "provenance": true
73
+ },
74
+ "sideEffects": false
75
+ }