Ext Ops Panel
Open dashboard

Home/Docs/Monitoring

Monitoring contract

Contract version 2.1.0 · payload schema_version 2.0

Two kinds of heartbeat. This page covers monitoring of a Chrome extension registration: your agent sends its report to POST /api/v1/monitoring/report. Web project environments in the panel use a separate POST /api/v1/heartbeat/report with a different token. The minimal environment report is {"overall":"ok"}; a custom metrics v2 payload is accepted too. Do not swap one endpoint for the other when you set up an agent. Section 8b describes the environment heartbeat.

Monitoring puts your extension under constant watch: a live traffic light on its card in the list, a base monitoring tab in Detail and a separate Telegram status message with edge alerts. It combines two independently enabled components: a push heartbeat (an agent on your service's host reports on a declared schedule; silence is itself a signal) and uptime pings (the panel checks your public URLs for availability).

↓ Download OpenAPI 3.1 (YAML) /monitoring-openapi.yaml · info.version 2.1.0

1. How it works (two components, one light)

An extension is "on monitoring" when monitoring is enabled and at least one component is configured. Otherwise its card shows a neutral gray dot.

1b. Heartbeats and the slot pool

Monitoring lives at the extension level as two independent flat lists that share one slot pool: heartbeats (push producers, formerly "stands") and uptime pings (pull checks). An extension can have several heartbeats – for example a main host and a paid worker. Each heartbeat pushes reports with its own bearer token and declares its own presentation (tabs, sections and Telegram lines) via its label. Uptime pings are a separate flat list owned by the extension, not nested under any heartbeat. The panel merges everything into one aggregate light (worst-of), one Detail surface and one summary Telegram message.

Heartbeats and pings share a single per-extension slot pool: one heartbeat costs one slot, one active ping costs one slot, and their sum must not exceed the plan's max_slots (Trial 3 · Studio 10 · Fleet 25). Exceeding the pool returns 409 slot_limit; after a downgrade the excess is suspended (pings first, heartbeats last). The response carries slots_used and max_slots so the UI can show the budget.

2. The traffic light

The aggregate is the worst-of roll-up across every enabled heartbeat's component and every active ping's light, where severities order as ok < warn < crit. A heartbeat's component is max(reported, watchdog): reported is the worst of the overall metric and the breached thresholds (6.2) from the last accepted report; watchdog is a freshness floor computed from the panel's own receive clock (agent clock skew is harmless).

StateColorWhen
off⚪ grayMonitoring not enabled, or no component configured.
paused⚪ grayEnabled but frozen (plan gate or manual pause). Reports are still stored.
green🟢All components ok.
yellow🟡Worst component warn.
red🔴Worst component crit.

Severity is canonical ok | warn | crit. Friendly aliases are normalized on intake: warning becomes warn; error and critical become crit.

3. Watchdog (missed heartbeats)

You declare the expected report interval (60s to 24h) when enabling the heartbeat. The watchdog then floors the light by freshness, with grace = max(60s, 25% of the interval) to absorb cron and network jitter:

Age of last accepted reportWatchdog floor
up to interval + graceok (green)
1 missed report (up to 2 × interval + grace)warn (yellow)
2 or more missed reportscrit (red)

A rejected report (401 / 413 / 422 / 429) never refreshes the watchdog, so a broken agent honestly escalates; the last rejection reason is visible in the monitoring settings. Recovery is instant: the first accepted report recomputes the light immediately.

4. Uptime pings

A ping is a named availability check of one public https URL. The panel performs GET with a 10s timeout, follows at most 5 redirects, reads no body beyond a minimal limit and treats a final 2xx-class status as success (204 and 206 count too). A final 3xx (redirect loop or limit), 4xx, 5xx, timeout or DNS/TLS/network error is a failure. A failed check is confirmed by one retry after ~15s before it counts, which absorbs single network blips.

Consecutive confirmed failuresPing light
0ok
1warn
2 or morecrit

Ping results appear as first-party metrics ping:<name> in the pings section (Uptime) of the base monitoring tab and as the reserved Telegram line pings: 🟢 api 210ms · 🟢 site 90ms · 🔴 docs (timeout). The number of pings per extension and the minimum interval depend on the plan:

PlanUptime pings per extensionMin ping interval
Trial35 min
Indie35 min
Studio101 min
Fleet251 min

Monitoring availability and the number of slots depend on your plan; the minimum heartbeat interval is 60s.

5. Putting an extension on monitoring

In the extension's Settings, monitoring block:

6. The report format

The report is the same metadata-driven v2 payload as the custom-metrics contract (tabs, sections, Telegram lines, metrics), pushed to POST /api/v1/monitoring/report with Authorization: Bearer <monitoring token>. Monitoring adds reserved ids:

Reserved idStatusMeaning
overallrequiredMetric of kind status: the verdict of the tick, severity ok | warn | crit. Feeds the aggregate. A report without a valid overall is rejected (422).
issuesrecommendedMetric of kind list: the current problems, items at the top level of the metric. Up to 5 items are quoted in alert texts.
ping:*forbiddenMetric-id namespace of the panel's own ping metrics; pushing it is rejected (422).
pingsforbiddenTelegram line id of the panel's ping line; pushing it is rejected (422).
monitoringpanel-owned tabBase tab id. Metrics may address it (dashboard.tab: "monitoring") without declaring it in tabs; a declaration is ignored.
basepanel-owned tabThe Basic tab id, reserved in the same way: a declaration is ignored.

Limits: payload at most 64 KB, at most 200 metrics, at most 2 reports per minute per source (excess gets 429 and does not count as a heartbeat).

A body over 64 KB gets 413 payload_too_large and is never cut and read as a shorter report. A report that breaks the rules above gets 422 invalid_report with every problem listed. Both reasons are kept as the heartbeat's last error:

HTTP/1.1 413 Payload Too Large
{ "code": "payload_too_large", "message": "report is over 64 KB; send fewer metrics or split them across heartbeats",
  "limit_bytes": 65536, "size_bytes": 71680 }

HTTP/1.1 422 Unprocessable Entity
{ "code": "invalid_report", "message": "report rejected",
  "errors": [ "metric \"overall\" must have kind \"status\" (got \"text\")",
              "metric id \"ping:x\" uses the reserved \"ping:\" namespace" ] }

6.1. Example report

{
  "schema_version": "2.0",
  "generated_at": "2026-07-12T10:19:58Z",
  "tabs":     [ { "id": "stand", "title": "Stand", "order": 5, "icon": "server" } ],
  "sections": [ { "tab": "stand",      "id": "docker", "title": "Docker", "order": 0 },
                { "tab": "monitoring", "id": "host",   "title": "Host",   "order": 10 } ],
  "telegram": { "header": "{sev} {name} monitoring",
                "lines": [ { "id": "host",   "order": 0, "prefix": "🖥 " },
                           { "id": "docker", "order": 1, "prefix": "📦 " } ] },
  "metrics": [
    { "id": "overall", "kind": "status", "state": "ok", "severity": "ok" },

    { "id": "issues", "kind": "list",
      "items": [ { "text": "RAM 91% (7.1/7.8G)", "severity": "warn" } ],
      "dashboard": { "show": true, "tab": "monitoring", "section": "host", "order": 0,
                     "render": "list", "name": "Issues" },
      "telegram":  { "show": false } },

    { "id": "ram", "kind": "percent", "label": "RAM", "value": 91, "severity": "warn",
      "dashboard": { "show": true, "tab": "monitoring", "section": "host", "order": 1,
                     "render": "kpi" },
      "telegram":  { "show": true, "line": "host", "pos": 10,
                     "format": "RAM {value}%", "sep": " · " } }
  ]
}

Successful response:

{ "accepted": true, "effective": "green", "next_expected_by": "2026-07-12T10:26:13Z" }

6.2. Thresholds on report metrics

Any numeric metric of a report, except overall, may carry thresholds (the block of the custom-metrics contract, section 5.4). The panel computes the severity of such a metric at intake and the heartbeat's light is the worst of overall and the breached thresholds: a metric over crit makes the heartbeat red even when overall is ok. recover works against the previous accepted report of the same heartbeat. A broken block gets 422 invalid_report with the reason in the list. A report without thresholds behaves as before.

{ "id": "queue", "kind": "number", "label": "Mail queue", "value": 41,
  "thresholds": { "direction": "above", "warn": 20, "crit": 100, "unit": "msgs", "recover": 15 },
  "dashboard": { "show": true, "tab": "monitoring", "section": "host", "order": 2, "render": "kpi" },
  "telegram": { "show": true, "line": "host", "pos": 20, "format": "queue {value}" } }

The panel draws it as 41 (threshold 20/100 msgs) on a yellow tile, and the heartbeat is yellow.

7. Integrating a monitor script (monitor.sh style)

If you already run a cron health-check script, add one best-effort POST at the end of its tick. A delivery failure must not break the tick: the watchdog will catch prolonged silence anyway.

# monitor.env
EXTOPS_REPORT_URL="https://extops.dev/api/v1/monitoring/report"
EXTOPS_REPORT_TOKEN="<monitoring token from Settings>"

# end of the tick: build the payload with jq from the values you already computed,
# then push it best-effort (never let delivery break the tick itself)
payload="$(jq -n --arg sev "$SEVERITY" '{
  schema_version: "2.0",
  generated_at: (now | todate),
  metrics: [ { id: "overall", kind: "status", state: $sev, severity: $sev } ]
}')"
code="$(curl -sS -m 10 -o /tmp/extops-report.json -w '%{http_code}' -X POST "$EXTOPS_REPORT_URL" \
  -H "Authorization: Bearer $EXTOPS_REPORT_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$payload" || echo 000)"
# 413 payload_too_large: over 64 KB; 422 invalid_report: .errors lists every problem
case "$code" in 413|422) logger -t monitor "panel: $code $(jq -c . /tmp/extops-report.json)" ;; esac

Map your script's severities to the canonical set (critical is accepted and normalized to crit) and feed your existing issue list into the issues metric. Set the declared interval equal to the cron period.

8. Telegram: separate status message and alerts

8b. Web project heartbeats

A web project has heartbeats of its own. Each one belongs to an environment (for example Production) and has its own hb_ token. The report rules are those of section 6; below is what differs.

POST /api/v1/heartbeat/report
Authorization: Bearer hb_<64 hex>
Content-Type: application/json

{"overall":"ok"}

8c. Who connects to your services and how

A web project can add port checks to a Service whose address is a host, for example a mail server. These checks are made by Ext Ops Panel itself. This section says who connects, what is sent, how often, and how a host can opt out. The agent contract above does not change.

9. The machine-readable contract

The formal description of the report endpoint, the payload schema, the reserved ids and the traffic-light semantics is the OpenAPI 3.1 file. It carries the same contract version (2.1.0).

↓ Download OpenAPI 3.1 (YAML) /monitoring-openapi.yaml