RunSignal for Python
Cron job and background job monitoring for Python, on OpenTelemetry. Get an alert when a Celery task or Celery beat schedule, an APScheduler job or a cron script is missed, fails, runs too long or processes nothing, and when a consumer or worker loop stops making progress: the failures no HTTP status code describes. No hosted service and no ping URLs: the alerts live in the backend you already use.
RunSignal records runs, items, heartbeats and the age of runs still in progress as OpenTelemetry metrics, with the same names as every other
RunSignal SDK, so the runsignal CLI provisions the same alerts for Python services as for .NET, Go, Java,
Node.js and Rust ones: New Relic alerts and dashboards, or Prometheus rules and a Grafana dashboard. The
main README covers the concepts, the alerts and the
backends.
Installation
pip install runsignal # core, depends only on opentelemetry-api
pip install "runsignal[celery]" # with Celery 5.3+
pip install "runsignal[apscheduler]" # with APScheduler 3.10+
Requires Python 3.9 or later. Install and configure the OpenTelemetry SDK and an exporter as usual
(opentelemetry-sdk, opentelemetry-exporter-otlp).
Quick start
from runsignal import default_registry
invoices = default_registry.workload(
"nightly-invoice",
schedule="0 2 * * *",
max_duration="30m", # or seconds, or a timedelta
min_processed=100,
severity="critical",
)
with invoices.run():
for order in unbilled_orders():
create_invoice(order)
invoices.processed()
Or as a decorator, for sync and async functions:
from runsignal import monitor
@monitor("nightly-invoice", schedule="0 2 * * *", max_duration="30m")
def generate_invoices():
for order in unbilled_orders():
create_invoice(order)
generate_invoices.workload.processed()
default_registry records on the global OpenTelemetry providers, which forward to the SDK once it is configured,
so it works even when it is created by an import first. For explicit providers, use
WorkloadRegistry(meter_provider=..., tracer_provider=...).
Recording work
| Call | Records |
|---|---|
with w.run(): / async with w.run(): |
One run. It succeeds when the block completes, is cancelled on asyncio.CancelledError, KeyboardInterrupt or SystemExit, and fails on any other exception, which propagates. The run's span is current inside the block. |
@w.monitor / @monitor(name, **expectations) |
One run per call, for sync and async functions. |
run = w.start() then run.succeed() / fail(exc) / fail_code(code, description) / cancel() |
A run ended by hand. Later calls are ignored. A run that is never ended stays in progress: it records no outcome, and its age on runsignal.workload.active_run_age keeps growing until the hung alert opens. |
w.processed(n) / w.item_failed(n) |
Items handled. |
w.beat() |
A heartbeat from a long-running loop. |
error.type is the exception's error_type attribute if it has one (such as "statement_missing"), otherwise
its fully qualified class name (ValueError, requests.exceptions.ConnectionError).
Celery
from celery import Celery
from celery.schedules import crontab
from runsignal.celery import instrument, workload_of
app = Celery("billing")
app.conf.beat_schedule = {
"nightly-invoice": {"task": "billing.tasks.generate_invoices", "schedule": crontab(hour=2, minute=0)},
}
instrument(app, workloads={"billing.tasks.generate_invoices": {"max_duration": "30m", "severity": "critical"}})
@app.task(bind=True)
def generate_invoices(self):
for order in unbilled_orders():
create_invoice(order)
workload_of(self).processed()
- What is recorded: every task execution, with no code in your tasks: success, failure with the exception's type (each retry counts as a failed run), cancellation when a task is revoked, and duration. Celery's own
celery.*tasks are skipped. - Naming: tasks are named after their task name (
billing.tasks.generate_invoices). - Celery beat:
instrument(app)registers every task inbeat_schedulewith itscrontabas the schedule, so a periodic task that never runs gets amissedalert. Interval schedules get no schedule. - Manifest: the worker writes it when it is ready if
RUNSIGNAL_MANIFEST_PATHis set. In CI, import the app and calldefault_registry.write_manifest_from_env("billing")instead; no broker is needed. - Prefork workers: configure the OpenTelemetry SDK in
worker_process_init, as for any OpenTelemetry instrumentation of Celery.
APScheduler
from apscheduler.schedulers.background import BackgroundScheduler
from runsignal.apscheduler import instrument
scheduler = BackgroundScheduler()
scheduler.add_job(generate_invoices, "cron", hour=2, minute=0, id="nightly-invoice")
instrument(scheduler, workloads={"nightly-invoice": {"max_duration": "30m", "severity": "critical"}})
scheduler.start()
- What is recorded: every run of every job, including jobs added after
instrument, for all schedulers (BackgroundScheduler,BlockingScheduler,AsyncIOScheduler, ...). - Naming: jobs are named after the
idyou gave them, or theirnamewhen APScheduler generated the id. - Schedules: cron triggers become the workload's schedule, with APScheduler's Monday = 0 converted to cron's numbering. Triggers that use
year,weekor days such as1st monget no schedule, with a warning. - Persistent job stores (SQLAlchemy, Redis, MongoDB) cannot store a wrapped function: decorate the job function with
@default_registry.monitor("nightly-invoice")instead.instrumentstill registers the schedule.
Cron scripts and loops
A script run by cron, a Kubernetes CronJob or systemd timer is one run per invocation:
with default_registry.workload("nightly-invoice", schedule="0 2 * * *").run():
main()
The missed alert needs a long-running process to report since_success, so for short-lived scripts declare
the workload in the service that exports continuously, or rely on the failed, overrun and low output
alerts. Long-running loops send heartbeats:
sync = default_registry.workload("stock-sync", expect_every="5m")
while True:
sync_stock()
sync.beat()
Manifest
default_registry.write_manifest_from_env("billing-service") # writes to RUNSIGNAL_MANIFEST_PATH when it is set
Then provision the alerts from CI with the runsignal CLI.
Exporting
New Relic needs delta temporality. With cumulative, a stopped workload never looks stopped:
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.eu01.nr-data.net:4317 # US: https://otlp.nr-data.net:4317
export OTEL_EXPORTER_OTLP_HEADERS=api-key=<your license key>
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta
Prometheus 3 receives OTLP at /api/v1/otlp/v1/metrics with the default (cumulative) temporality. Generate
its rules with runsignal export prometheus and a dashboard with runsignal export grafana.
Development
python -m venv .venv && .venv/bin/pip install -e ".[celery,apscheduler]" opentelemetry-sdk pytest
.venv/bin/pytest
The tests include the cross-language conformance vectors in spec/conformance.
Links
Metadata
Release files for runsignal 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| runsignal-0.3.0.tar.gz | 20.8 kB | With provenance | Details for runsignal-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| runsignal-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for runsignal-0.3.0-py3-none-any.whl |
Total release size: 41.7 kB
Release files / runsignal-0.3.0.tar.gz
| Download URL | runsignal-0.3.0.tar.gz |
|---|---|
| Size | 20.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
461f6103e8a0e958c3e52132e21b67e5406f9285569093cd799ae40c9d8e8331
|
|
BLAKE2b-256 checksum How to use checksums |
bb4946250a9f36706d49f9a3a0829da8687180b26ddea0c7a26676000b804738
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / runsignal-0.3.0-py3-none-any.whl
| Download URL | runsignal-0.3.0-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0bf7ffa47eeb5fe99ec47a4ca59c526f9a1e8ac0c171f052a5552d17df0cf647
|
|
BLAKE2b-256 checksum How to use checksums |
7827feef3d2163181d8d924cbbb3f4a9c542d3a37df0ebaec125bbea3a82c05d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for runsignal 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for runsignal-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.