@last9/otel-cron
v0.1.0
Published
OTel FaaS-compliant instrumentation for cron job observability
Readme
@last9/otel-cron
Cron jobs have an observability problem. They miss their window, throw unhandled exceptions, or silently stop running — and you find out from users, not alerts. This library fixes that.
Wrap your job function with withCronJob and get OTel FaaS-compliant spans and metrics. No changes to your job logic required.
import { withCronJob } from '@last9/otel-cron';
await withCronJob(
{ name: 'send-digest', cron: '0 8 * * *' },
async () => {
await sendDigestEmails();
}
);Uses the global OTel API — configure your providers as you normally would and withCronJob picks them up.
Installation
npm install @last9/otel-cron @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-httpPeer dependency: @opentelemetry/api ^1.9.0.
Configuration
Set these environment variables before starting your process. Never hardcode them — use .env files locally and your secrets manager in production.
# Where to send telemetry
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318
# Auth header — keep this out of source control
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token-here"
# Identifies your service in traces and metrics
OTEL_SERVICE_NAME=my-appBootstrap the SDK once at process startup, before any withCronJob calls:
// instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(), // reads OTEL_EXPORTER_OTLP_ENDPOINT + HEADERS
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(), // same env vars
exportIntervalMillis: 60_000,
}),
});
sdk.start();Run it with --require so it loads before your application code:
node --require ./instrumentation.js your-cron-runner.jsOr with tsx / ts-node:
node --require tsx/cjs --require ./instrumentation.ts your-cron-runner.tsThe OTLP exporters read OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS automatically — no URL or auth token in code.
Missed-run alerting
The faas.last_success_time gauge records a Unix timestamp after every successful run. Wire it to a dead-man alert:
time() - faas_last_success_time{faas_name="send-digest"} > 90000That fires if the daily digest hasn't succeeded in 25 hours — no scheduler integration, no sidecar, no external ping service.
Timeouts
await withCronJob(
{ name: 'generate-report', cron: '0 6 * * *', timeout: 30_000 },
async () => {
await generateReport();
}
);Exceeding the timeout throws FaaSTimeoutError and increments faas.timeouts rather than faas.errors. Timeouts are a distinct failure mode worth tracking separately. The underlying function keeps running after the error is thrown; use an AbortController if you need hard cancellation.
Signals
| Signal | Type | Description |
|--------|------|-------------|
| faas.invocations | Counter | Every invocation |
| faas.errors | Counter | Non-timeout failures |
| faas.timeouts | Counter | Timeout breaches |
| faas.invoke_duration | Histogram (seconds) | Wall-clock duration |
| faas.last_success_time | ObservableGauge (Unix s) | Timestamp of last success |
| Span | SERVER · faas.trigger=timer | FaaS semantic convention |
Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| name | string | Yes | Job identifier. Becomes faas.name on every signal. |
| cron | string | Yes | Cron expression, emitted as faas.cron on the span. |
| timeout | number | No | Deadline in milliseconds. |
Notes
State is keyed on globalThis under Symbol.for('@last9/otel-cron/state'). Two copies of this package in the same process share state — the right behavior in monorepos. Run npm dedupe if you see duplicate metric registrations.
OTel API v1 only (^1.9.0). v2 compatibility requires a new major.
