Skip to main content

RunSignal for Python

PyPI Python versions license

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 in beat_schedule with its crontab as the schedule, so a periodic task that never runs gets a missed alert. Interval schedules get no schedule.
  • Manifest: the worker writes it when it is ready if RUNSIGNAL_MANIFEST_PATH is set. In CI, import the app and call default_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 id you gave them, or their name when 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, week or days such as 1st mon get 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. instrument still 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.

Specification · Changelog · Other languages

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)

Source distribution for runsignal 0.3.0
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)

Table of built distributions (wheels) for runsignal 0.3.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page