Skip to content

oozman/pingly

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Pingly logo

Pingly

A tiny dead-man's switch for cron jobs and background scripts, running on Cloudflare Workers.

Your job pings a unique URL after each successful run. Miss the window and Pingly sends a Telegram alert. When pings resume, it sends a recovery message. That's the whole product.

No web UI, no database, no servers to babysit — just one Worker and one KV namespace.


How it works

  1. You register a check and get back a unique pingUrl.
  2. Your cron job hits that URL at the end of each successful run.
  3. Every minute, Pingly sweeps for overdue pings.
  4. Miss the window → 🔴 "… is DOWN" in Telegram.
  5. Next successful ping → 🟢 "… is UP".

Why use it

  • Zero ops — deploy once, then forget about it.
  • Free-tier friendly — fits inside Cloudflare Workers' free plan for typical use.
  • Self-hosted — your monitoring data lives in your own Cloudflare account.
  • One small file — short, plain JavaScript. Easy to read, fork, and adapt.

Quickstart

You'll need:

  • A Cloudflare account (free tier is fine)
  • A Telegram bot token from @BotFather
  • Your Telegram chat id (DM @userinfobot to get it)
  • Node.js with npx
git clone <this-repo>
cd pingly

# 1. Copy the example wrangler config. This file is gitignored so your
#    KV ids and chat id stay local.
cp wrangler.toml.example wrangler.toml

# 2. Create the KV namespaces. Wrangler prints the new ids — paste them
#    into the [[kv_namespaces]] block in wrangler.toml.
npx wrangler kv namespace create PINGLY_KV
npx wrangler kv namespace create PINGLY_KV --preview

# 3. Add your secrets (interactive prompts).
npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put ADMIN_TOKEN              # any long random string

# 4. Set your default Telegram chat id in wrangler.toml under [vars].

# 5. Deploy.
npx wrangler deploy

You'll end up with a live URL like https://pingly.<your-subdomain>.workers.dev.

Register your first check

export ADMIN_TOKEN='the-token-you-set-above'
export PINGLY_URL='https://pingly.<your-subdomain>.workers.dev'

curl -s -X POST $PINGLY_URL/checks \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"backup-job","intervalSec":3600,"gracePeriodSec":300}'

The response includes a pingUrl. Drop it at the end of your cron script:

0 * * * * /path/to/backup.sh && curl -fsS https://pingly.example.workers.dev/ping/<uuid>

That's it — if the script runs successfully, Pingly sees the ping. If it doesn't, you get a Telegram message within a minute or two of the deadline.


Local development

cd pingly
npx wrangler dev

For local secrets, create pingly/.dev.vars (gitignored):

TELEGRAM_BOT_TOKEN=your_bot_token
ADMIN_TOKEN=your_admin_token

Trigger the cron sweep manually instead of waiting a minute:

curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*/1+*+*+*+*&time=$(date +%s)"

API reference

Method Path Auth Description
GET / none Liveness probe — { ok, version }
GET HEAD POST /ping/<id> none Record a heartbeat
GET /checks admin List all registered checks
POST /checks admin Create a check
PUT /checks/<id> admin Update a check (partial)
DELETE /checks/<id> admin Remove a check

Admin endpoints require Authorization: Bearer <ADMIN_TOKEN>. The ping URL itself is unauthenticated — its UUID is the secret, so don't share it.

POST /checks

Field Type Required Default Description
name string yes Label for the check (max 80 chars)
intervalSec number yes How often you expect a ping, in seconds (min 10)
gracePeriodSec number no 60 Extra slack added to intervalSec before alerting
chatId string no null Per-check Telegram chat override

Keep gracePeriodSec ≥ 60 to absorb KV's eventual-consistency window (reads can be up to ~60s stale). Tighter values risk false alarms.

PUT /checks/<id>

Partial update — send only the fields you want to change. An empty body returns 400.

Field Type Description
name string New label
intervalSec number New expected interval
gracePeriodSec number New grace window
chatId string | null String to set a per-check chat; null to clear

Operational fields (id, createdAt, lastPingAt, lastPingIp, alerted) are not editable.

Examples

# List checks
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" $PINGLY_URL/checks

# Update interval
curl -s -X PUT \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"intervalSec": 600}' \
  $PINGLY_URL/checks/<id>

# Delete
curl -s -X DELETE \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  $PINGLY_URL/checks/<id>

Alert format

Telegram messages use HTML formatting; durations and IPs render in a monospace block.

Down

🔴 backup-job is DOWN

Last Ping: 62min ago
IP: 203.0.113.42
Expected: every 1hr

Up

🟢 backup-job is UP

Last Ping: 62min ago
IP: 203.0.113.42

Each outage produces exactly one DOWN and one UP message — no spam, no re-alerts until recovery.

Pluggable notification channels

Telegram is the default. The notification layer uses a factory pattern so you can swap in any HTTP-callable channel — email, SMS, Slack, Discord, generic webhook, etc.

To add one, look at the Notification factory section in worker.js:

  1. Implement create<Name>Notifier(env) returning { send(target, message) }.
  2. Register it in the switch inside createNotifier(env).
  3. Set NOTIFIER=<name> in wrangler.toml under [vars] (or per-environment).
  4. Add any required secrets via wrangler secret put.

Stubs for email and sms are already in the file as starting points. Each notifier receives the same structured Message ({ level, title, fields }) and decides how to render it for its channel.

Recovery fires either:

  • On the next ping — instantly, the moment the watched job comes back.
  • In the cron sweep — fallback for the rare case where KV reads return a stale snapshot.

Check record shape

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "backup-job",
  "intervalSec": 3600,
  "gracePeriodSec": 300,
  "chatId": null,
  "lastPingAt": 1715300000000,
  "lastPingIp": "203.0.113.42",
  "alerted": false,
  "createdAt": 1715290000000
}
  • lastPingAt, lastPingIp — populated on the first ping. Until then the check is "armed but quiet" and won't alert.
  • alertedtrue while an outage is active. Only the ping handler and the cron sweep can flip it.
  • chatId — overrides the global TELEGRAM_CHAT_ID for this check only.

Limitations

  • No web UI, no ping history.
  • Telegram is the only notification channel.
  • One alert per outage; no re-alerts until recovery.
  • A DELETE racing with the cron sweep can briefly resurrect the deleted record. Delete again to clear.

Observability

npx wrangler tail pingly

Useful log lines:

ping received: <name> [<id>] from <ip>
ping received (recovery): <name> [<id>] from <ip>
check overdue: <name> [<id>]
check recovered (sweep): <name> [<id>]
check updated: <name> [<id>] fields=<csv>
check deleted: <name> [<id>]
telegram notifier: sent to <chat_id>
telegram notifier: non-2xx <status>: <body>

Verify cron registration after deploy

npx wrangler triggers list

Or in the Cloudflare dashboard: WorkerspinglyTriggers → confirm */1 * * * * is listed.

About

A tiny dead-man's switch for cron jobs and background scripts, running on Cloudflare Workers.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors