Monitors as code
Keep your monitors in openping.yml, next to the code they watch. They are reviewed like code, and the repo stays the source of truth.
An example
This is the file one of our own products uses: a web check, an agent test, a heartbeat and an MCP monitor.
service: taskos
defaults:
regions: [mumbai, frankfurt, virginia]
every: 1m
alert: taskos-oncall
monitors:
- name: Web app
type: http
url: https://tasks.supertuned.ai/api/health
expect:
status: 200
latency: { under: 1500ms }
- name: Chat answers
type: agent
endpoint: https://tasks.supertuned.ai/api/chat
auth: { bearer: secret.TASKOS_TEST_TOKEN }
every: 1h
cases:
- ask: "What's due today?"
expect:
- answered: { within: 20s }
- not_contains: "error"
- judge: "Lists the tasks due today, or says there are none."
- name: Meeting notes read
type: heartbeat
schedule: "50 10 * * 1-5"
timezone: Asia/Kolkata
grace: 15m
- name: OpenPing MCP
type: mcp
url: https://mcp.openping.ai
auth: { oauth: client_credentials, secret: secret.OPENPING_MCP_CLIENT }
expect:
tools: { includes: [list_monitors, create_monitors] }
on_change: alert
call:
tool: list_monitors
args: { limit: 1 }The flow
openping init # reads the project, writes openping.yml
openping test # runs every monitor now, from this machine
openping deploy # shows the plan, asks, then applies itdeploy prints what it will do before it does it: + add, ~ change and - remove. Only monitors that came from this file, in this service, are ever changed or removed. Monitors you made in the app are left alone.
The file
service: the name of the product or part these monitors belong to.defaults:regions,every,alert(an alert rule’s name) andtimeout, used by any monitor that doesn’t set its own.monitors: up to 500. Each has anameand atype; the rest depends on the type.
| Key | Used by | Meaning |
|---|---|---|
url, endpoint, host, domain | All but heartbeat | What to check |
every, timeout, grace | All | A duration: 30s, 1m, 1h, 1500ms |
regions, severity, tags, alert | All | Where from, how loud, labels, and which alert rule |
method, headers, body | http | The request to send |
auth | http, mcp, agent | { bearer: secret.NAME }, basic, header, or { oauth: client_credentials, secret: secret.NAME } |
expect | http, mcp, gateway | status, latency, contains, not_contains, regex, json, header, cert_days, tools, on_change, first_token |
cases, samples, target, protocol, model | agent | The questions and how answers are judged |
schedule, timezone | heartbeat | A cron expression and its time zone |
call | mcp | The safe test call: tool, args |
models, api_key | gateway | Which models get the tiny streamed reply |
records, mail | dns | Record types to watch; also check SPF and DMARC |
port | tcp, cert | The port to connect to |
A key the file doesn’t know is an error, not a silent skip. A mistake is reported with its place: monitors[2].every: use a duration like 30s, 1m or 1h.
Secrets never go in the file
Write secret.NAME where a token goes. On our side the value comes from your organisation’s secrets. When you run openping test locally, secret.NAME is read from the environment variable $NAME. deploy tells you which secrets are missing before it applies anything.
In CI
name: openping
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx openping test
- run: npx openping deploy --yes
env:
OPENPING_API_KEY: ${{ secrets.OPENPING_API_KEY }}See the CLI for every command.