Skip to main content

Self-hosted uptime & status-page tool: scheduled health checks, incident tracking, and Discord alerting.

Project description

pulsewatch

tests codecov

A self-hosted uptime & status-page tool. Pings the services you configure on their own schedule, tracks response history, opens/resolves incidents automatically, and alerts you on Discord the moment something goes down — then again when it recovers.

Built with FastAPI + SQLite + APScheduler. No external database or paid service required to run it.

Why this exists

Every team running more than a couple of services eventually needs to know immediately when one goes down, not when a user complains. Tools like UptimeRobot and Statuspage.io solve this commercially — this is a from-scratch implementation of the same core mechanics: scheduled health checks, failure thresholds (so one blip doesn't page you), incident tracking, and alerting.

Features

  • Configurable list of services to monitor (config.yaml), each with its own check interval and failure threshold
  • Monitor upstream dependencies as well as your own services: AWS region health, database connectivity (SELECT 1), or any third-party status API — the dashboard and alerts make clear which failures are yours vs an upstream's
  • Automatic incident creation when a service crosses its failure threshold, and automatic resolution on recovery
  • Pluggable alert channels (Discord, Slack, email) on both down and recovery events — enable as many as you like
  • Live dashboard (/) showing current status, 24h uptime %, a per-service response-time sparkline (last hour, via Chart.js), and incident history
  • Check from multiple named regions — independent worker processes that each report their own per-region status for a service
  • JSON API (/api/status, /api/services/{id}/history, /api/services/{id}/response-times) for scripting or integration elsewhere

Installing

pulsewatch is a proper installable package. Install it in editable mode from a clone:

python -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install -e .

This puts a pulsewatch command on your PATH. Run it with no arguments to see the full help menu:

pulsewatch --help

Then copy the example config and fill in your own values (webhook URLs, etc.):

cp config.example.yaml config.yaml

config.yaml is gitignored, so your real webhook URLs and connection strings never get committed — keep secrets there, not in config.example.yaml.

Run with Docker

As an alternative to the pip install above, you can run pulsewatch in a container. You only need Docker — no local Python.

cp config.example.yaml config.yaml   # create your config first (edit as needed)
docker compose up -d                 # build the image and start the app

Then open http://localhost:8000. The dashboard and API are served on port 8000.

  • Config is bind-mounted from your local config.yaml (read-only). Edit it on the host and docker compose restart to pick up changes.
  • The SQLite database persists across restarts and rebuilds in the pulsewatch-data named volume — your ping history and incidents survive docker compose restart / up / down. (Use docker compose down -v to also wipe the data volume.)
docker compose logs -f     # follow logs
docker compose down        # stop (keeps the data volume)

Prefer plain Docker? The equivalent without Compose:

docker build -t pulsewatch .
docker run -d --name pulsewatch -p 8000:8000 \
  -v "$PWD/config.yaml":/data/config.yaml:ro,z \
  -v pulsewatch-data:/data \
  pulsewatch

config.yaml must exist before you start the container — otherwise Docker creates an empty directory in its place. The ,z on the config mount relabels it for SELinux hosts (Fedora/RHEL) and is a harmless no-op elsewhere.

The CLI

pulsewatch init                              # write a default config.yaml here
pulsewatch add --name "My API" \
               --url https://api.example.com/health \
               --interval 30 --threshold 3   # append a service, no hand-editing
pulsewatch serve                             # start the app + background monitor
  • pulsewatch init creates a config.yaml in the current directory (use --force to overwrite an existing one).
  • pulsewatch add appends a service to the active config.yaml (--interval and --threshold are optional and default to 30s / 3 failures). If no config exists yet it creates one in the current directory.
  • pulsewatch serve replaces the old uvicorn app.main:app command. It accepts --host, --port, --reload, and --region (see Multiple regions).
  • pulsewatch worker --region NAME runs a checks-only worker for an additional region, no web server.

Then open http://127.0.0.1:8000

Configuring services

Start from the committed template (cp config.example.yaml config.yaml), then hand-edit it or use pulsewatch add. pulsewatch looks for config in this order:

  1. ./config.yaml (the current working directory)
  2. ~/.pulsewatch/config.yaml (per-user fallback)
services:
  - name: "My API"
    url: "https://api.example.com/health"
    check_interval_seconds: 30
    failure_threshold: 3

alerts:
  - type: discord
    webhook_url: "https://discord.com/api/webhooks/..."
  - type: slack
    webhook_url: "https://hooks.slack.com/services/..."
  - type: email
    to: "oncall@example.com"
    from: "pulsewatch@example.com"     # optional; defaults to the SMTP user

failure_threshold is how many consecutive failed checks are needed before an incident opens — this avoids firing an alert on a single network blip.

Monitoring upstream dependencies

Every service defaults to check_type: http (a plain URL ping, unchanged). Set check_type: dependency with a dependency_kind to watch an upstream provider instead. These are tagged DEPENDENCY on the dashboard and alert as "Upstream dependency … is degraded" rather than "Your service is down", so you can tell your own outage from a provider's at a glance.

aws_status — polls the public AWS Health feed (https://health.aws.amazon.com/public/currentevents) and reports down only if your region (and optionally specific services) are impacted, not on any AWS event anywhere. region is required; services is an optional filter.

  - name: "AWS us-east-1"
    check_type: dependency
    dependency_kind: aws_status
    region: "us-east-1"
    services: ["EC2", "S3"]     # optional
    failure_threshold: 1

database — runs a lightweight SELECT 1 over a SQLAlchemy connection string instead of an HTTP request. Install the matching driver for your DB (e.g. psycopg2-binary for Postgres, PyMySQL for MySQL; SQLite needs none). Credentials are never shown on the dashboard or API — the target is redacted to scheme://host/db.

  - name: "Primary Postgres"
    check_type: dependency
    dependency_kind: database
    connection_string: "postgresql://user:pass@localhost:5432/mydb"

custom_api — fetches a JSON endpoint and asserts a nested field equals an expected value (dotted path), not just the HTTP status. Great for a provider's own status API (e.g. Stripe is healthy when status.indicator == "none").

  - name: "Stripe API"
    check_type: dependency
    dependency_kind: custom_api
    url: "https://status.stripe.com/api/v2/status.json"
    json_field: "status.indicator"
    expected_value: "none"

To add a new dependency kind, write a checker in checks.py returning a CheckResult and register it in DEPENDENCY_CHECKERS.

Alert channels

Both down and recovery events are sent to every channel in the alerts: list. With no channels enabled, alerts are logged to the console — so the project still runs and is demoable with zero setup.

Type Config keys Notes
discord webhook_url Server Settings → Integrations → Webhooks → New
slack webhook_url A Slack Incoming Webhook URL
email to, from (optional) SMTP server/credentials come from env vars (below)

Email keeps secrets out of config.yaml by reading the SMTP connection from the environment:

Variable Default Purpose
PULSEWATCH_SMTP_HOST SMTP server (required to send email)
PULSEWATCH_SMTP_PORT 587 SMTP port
PULSEWATCH_SMTP_USER Username (enables login if set)
PULSEWATCH_SMTP_PASSWORD Password (enables login if set)
PULSEWATCH_SMTP_FROM user Fallback From address
PULSEWATCH_SMTP_STARTTLS true Set false to disable STARTTLS

To add a channel of your own, subclass AlertChannel in alerts.py, implement send(message), and register it in _CHANNEL_BUILDERS.

Multiple regions

You can check your services from several named regions — really just independent worker processes (there's no real geographic distribution). Each worker runs its own scheduler, tags every result with its region name, and writes to the one shared database; the dashboard shows a per-region breakdown for any service that more than one region reports on.

Declare the regions in config.yaml:

regions:
  - name: "us-east"
  - name: "eu-west"

Then run one process per region, all pointed at the same config and database:

pulsewatch serve  --region us-east    # web dashboard + the us-east worker
pulsewatch worker --region eu-west    # an extra worker, no web server
  • pulsewatch serve --region NAME runs the dashboard and a built-in worker for NAME (default local, which is also what you get with no regions: declared — so single-region setups are unchanged).
  • pulsewatch worker --region NAME runs a checks-only worker for NAME.
  • Incidents and alerts are per-region (a down alert reads … is DOWN [region: us-east]), and each region keeps its own uptime %.

Under Docker, run a container per region sharing the config mount and the pulsewatch-data volume — see the commented worker-eu-west service in docker-compose.yml. The shared SQLite database uses WAL mode with a busy timeout so several worker processes can write to it at once.

Demoing the failure/recovery flow

The included config.yaml ships with a "Flaky Demo Service" pointed at https://httpbin.org/status/200,500, which randomly returns a failing status — so you'll see real incidents open and resolve within a few minutes of running it, without needing to break anything yourself.

For a live demo: run the app, wait for a couple of failed checks on the flaky service, and watch the dashboard status flip to DOWN and an incident appear — then watch it self-resolve once a check succeeds again.

Architecture

pyproject.toml            # packaging, dependencies, `pulsewatch` entry point
Dockerfile                # slim-Python image running `pulsewatch serve`
docker-compose.yml        # app + config mount + persistent DB volume on :8000
config.example.yaml       # copy to config.yaml and fill in your values
src/pulsewatch/
├── cli.py        # click CLI: init / serve / worker / add
├── config.py     # config discovery (CWD → ~/.pulsewatch) + regions helper
├── main.py       # FastAPI routes, dashboard rendering, startup wiring
├── monitor.py    # per-region scheduler: runs checks, manages incident state
├── checks.py     # pluggable checks (http / aws_status / database / custom_api)
├── models.py     # Service / RegionStatus / PingLog / Incident tables
├── database.py   # SQLite engine (WAL, multi-process) + session + migrations
├── alerts.py     # pluggable alert channels (Discord / Slack / email)
└── static/
    └── dashboard.html

Tests

pip install -e ".[dev]"
pytest                                    # run the suite
pytest --cov=pulsewatch --cov-report=term-missing   # with coverage

The suite mocks all external I/O (httpx, SMTP, DB engines), so it runs offline and deterministically — no real calls to AWS, databases, or the network. It covers the incident state machine (threshold + recovery), every check type and alert channel, the API endpoints (via FastAPI's TestClient), and app startup/shutdown. GitHub Actions runs it on every push and pull request (test.yml) on Python 3.11 and uploads coverage to Codecov.

CI badge note: Actions is currently blocked on this account by an unrelated GitHub billing issue (a support ticket is open to resolve it) — the badge will show red until that's cleared. This has no effect on the tool itself: all 84 tests pass locally, which you can verify yourself with the commands above.

The Codecov badge also needs a one-time (free) sign-in at codecov.io with your GitHub account to activate for this repo.

Possible extensions

  • SMS / PagerDuty / webhook alert channels (drop-in AlertChannel subclasses)
  • More dependency kinds (drop-in checkers in DEPENDENCY_CHECKERS)
  • Real geographic distribution for regions (currently independent processes, not actually run from different locations)
  • Public-facing read-only status page (auth-gated admin view)
  • Postgres support for multi-instance deployments

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pulsewatch-1.0.0.tar.gz (43.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pulsewatch-1.0.0-py3-none-any.whl (30.1 kB view details)

Uploaded Python 3

File details

Details for the file pulsewatch-1.0.0.tar.gz.

File metadata

  • Download URL: pulsewatch-1.0.0.tar.gz
  • Upload date:
  • Size: 43.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for pulsewatch-1.0.0.tar.gz
Algorithm Hash digest
SHA256 2c8199f13c0ac50b440dbe38fb3c6ab424649ace8271a67c20e01735cfae04c3
MD5 57edd58d18a8948f2512f830294f4797
BLAKE2b-256 c68ce312cfba728ea8069312f0a52245095b03f92a9f4f97412714a439caa68a

See more details on using hashes here.

File details

Details for the file pulsewatch-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pulsewatch-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 30.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for pulsewatch-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88d4f3efd75a04dadda42b06c817d108a922337ad9866257cd188d2846e64296
MD5 9618bd48237295b8e098b56b963783d9
BLAKE2b-256 2dd267df72ee864f8b7bfde5eb74dcc9f0d469f1618150e9db9c9b7d059cf86b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page