Operational alert schema¶
This contract applies to Prometheus rules, Loki ruler alerts, GitHub Actions events, and other sources that enter the incident platform.
Existing Prometheus rules migrate incrementally. CI records their current deficiencies in a baseline and rejects new deficiencies.
Required labels¶
| Label | Purpose | Example |
|---|---|---|
severity |
Operator urgency | critical |
service |
Stable affected service | github-runners |
component |
Subsystem within the service | runner-controller |
environment |
Deployment boundary | homelab |
owner |
Accountable team or owner | homelab |
category |
Operational failure class | availability |
notify |
Delivery lane | page or ticket |
In the current Alertmanager tree, notify=page is required to select Discord.
notify=ticket expresses intent for the OneUptime / operator inbox lane; it does
not create a route. Only explicit service/severity/alert-name allowlists select
the incident adapter, and unmatched alerts reach blackhole. Grafana Alert Triage
visibility is not proof of notification delivery. continue: true evaluates
later sibling routes, not receiver-health-based failover. See the
current receiver matrix.
Do not encode the delivery lane only in severity: some criticals stay tickets
(for example GitHub Actions queue), and some pages are capacity criticals on OS
volumes.
Every alert must also retain at least one stable target or domain dimension from
the expression, source payload, or explicit labels. Common dimensions are
instance, host, cluster, repository, runner_group, storage_pool, and
backup_target.
Labels are machine fields. Do not put prose, timestamps, generated URLs, or secret values in labels.
Optional labels¶
host: canonical inventory host ID when it differs frominstance;cluster: stable cluster or fleet ID;repository:owner/repositoryfor repository-scoped automation;workflow: stable workflow file or logical workflow name;runner_group: stable runner placement group;storage_pool: stable storage pool ID;backup_target: stable backup destination ID;backup_service: manifest-backed restic job ID whenservice=backup;restore_service: tier-1 repository selected by the latest restore drill;secret: allowlisted secret identifier or role, never a secret value;inhibit_key: shared failure mode for warning/critical threshold pairs;capacity_role:os(boot/system volumes that may page) orbulk(media/content mounts that are ticket-only);correlation_key: durable incident correlation path for the OneUptime adapter;source: source system when the event did not originate in Prometheus;schema_version: event contract version for non-Prometheus adapters.
Severity¶
Allowed values are:
info: retained or routed to low-priority channels; not sent to the incident platform by default;warning: actionable degradation that needs planned operator attention;critical: active outage, data-protection risk, security impact, or imminent service loss requiring prompt intervention.
Do not encode severity in the alert name. Warning and critical threshold pairs
use separate rules when their for duration or behavior differs, and share an
inhibit_key.
Required annotations¶
| Annotation | Purpose |
|---|---|
summary |
One-sentence operator-facing symptom |
description |
Evidence, threshold, duration, and likely impact |
dashboard_url |
Best Grafana investigation surface |
runbook_url |
Version-controlled response procedure |
Required annotations must be non-empty. URLs must use HTTPS except for explicitly internal test fixtures.
Optional annotations¶
| Annotation | Purpose |
|---|---|
logs_url |
Scoped Grafana Explore or log dashboard link |
action_url |
Bounded diagnostic/remediation entry point |
investigation_url |
Other authenticated investigation surface |
prometheus_url |
Prometheus expression or rule link |
action_url must not encode a secret or execute a destructive action on GET.
It should open an authenticated approval or dispatch surface.
Naming conventions¶
Services and components¶
- Use lowercase kebab-case.
- Use a durable operational capability, not a container name that may change.
- Examples:
github-runners,backup,monitoring,storage. - Components refine the service:
runner-controller,restic,alertmanager,postgresql.
Hosts and instances¶
hostuses the immutable inventory ID, such asinfra-services.instanceis the scrape/source identity. Prefer the inventory ID for node-level metrics instead of an IP address.- Do not rename hosts to describe a temporary role.
Ownership¶
owner is a stable, routable identifier. The default for lab infrastructure is
homelab. Do not use a person's display name or chat handle.
Categories¶
Use one of these initial categories:
availability;capacity;performance;data-protection;security;configuration;automation;dependency.
Adding a category requires updating this contract and its linter tests.
Fingerprints and correlation¶
Prometheus/Alertmanager's per-alert fingerprint is the alert identity. Do not replace it with a hash of formatted text.
Incident correlation uses a versioned ordered tuple:
Examples:
v1/homelab/github-runners/runner-controller/availability/linux
v1/homelab/storage/filesystem/capacity/primary
v1/homelab/backup/restic/data-protection/infra-services/monitoring
v1/homelab/plex/server/availability/media-host-01
The domain mapping is version controlled with the OneUptime workflow. Changing the tuple requires a new version so active incidents do not silently merge or split.
Alertmanager groupKey and alert fingerprint are retained in the timeline for
traceability.
Firing and recovery events¶
- The first firing event creates the alert and, if needed, its incident.
- Repeated firing with the same fingerprint updates
last_seen, evidence, and duration; it does not create another alert. - A resolved event updates the same fingerprint.
- Partial group resolution leaves the incident open.
- The incident resolves only when all attached fingerprints are resolved.
- A later firing event after full resolution reopens the same operational correlation or creates a linked recurrence, according to the versioned OneUptime workflow policy.
Alertmanager receivers for the incident platform must set send_resolved: true.
Non-Prometheus sources¶
GitHub Actions and custom checks should use this canonical source envelope as a
translation contract. It is not a directly accepted API payload. The current
private /v1/alertmanager endpoint accepts an AlertmanagerWebhook, not this
shape; this document does not provide or claim an implemented generic translator.
The example intentionally selects notify: ticket, consistent with runner alert
policy. Critical severity does not imply paging, and the lane alone does not
guarantee a receiver:
schema_version: v1
source: github-actions
source_event_id: "repository/run-id/job-id"
status: firing
starts_at: "2026-07-29T20:00:00Z"
ends_at: null
labels:
severity: critical
notify: ticket
service: github-runners
component: runner-controller
environment: homelab
instance: runner-host-01
owner: homelab
category: availability
runner_group: linux
annotations:
summary: "No eligible Linux runner is online"
description: "All Linux x64 runners have been unavailable for ten minutes."
dashboard_url: "https://grafana.infra.realemail.app/d/gha-fleet-overview"
logs_url: "https://grafana.infra.realemail.app/d/gha-runner-logs"
runbook_url: "https://github.com/spadoople/homelab/blob/main/docs/runbooks/github-actions-observability.md"
action_url: "https://github.com/spadoople/homelab/actions/workflows/runner-fleet-dispatch.yml"
A future source translator must derive a deterministic fingerprint from source,
source_event_id, and the stable affected-object identity, excluding status,
timestamps, summaries and descriptions. Resolved events must reuse the same
source_event_id and fingerprint. The current adapter consumes the supplied
Alertmanager fingerprint; it does not compute this source-envelope mapping.
Translation and acceptance boundary¶
Before enabling any non-Prometheus producer, review and test its translator:
- map source semantics to an explicitly approved alert name, service, component,
category, stable domain labels and matching
correlation_key; do not invent an allowlisted alert name merely to bypass validation; - retain the intentional
notifylane and verify receiver selection separately from severity; direct submission to the adapter does not traverse Alertmanager or deliver a Discord page; - construct the Alertmanager webhook fields (
receiver,version,groupKey,status, and exactly one member ofalertswithstatus,labels,annotations,startsAt,endsAt, andfingerprint), rather than POSTing the envelope above. Both top-levelstatusandalerts[0].statusare required (firingorresolved);alerts[0].statusmust equal the top-levelstatus; neither omitting per-alert status nor sending mismatched statuses is accepted; - convert snake-case timestamps and firing
ends_at: nullto the supported Alertmanager representation, preserving identity across firing and resolution; - do not blindly copy
source,source_event_idorschema_versioninto labels: the current parser rejects unsupported labels and enforces domain, correlation, annotation and URL allowlists in addition to the webhook shape; - retain private-network and bearer-token ingress restrictions. Independently accept repeat, firing, recovery, correlation and intended delivery behavior.
Conforming to this abstract envelope is source-contract validation, not proof of an implemented integration or successful delivery. Any new route, source or allowlist expansion requires separate review; this example authorizes none.
Validation policy¶
scripts/lint-alert-schema.py
validates
Prometheus rule files, not generic event ingestion. The offline
tests/test_ws4_monitoring_contracts.py also parses this fenced example against
the shared required-label/annotation contract. The service-local
services/oneuptime-incident-adapter/tests/test_ws4_envelope_boundary.py tests the
actual adapter's rejection of the untranslated envelope using the same fixture.
These checks send no events.
The baseline permits known legacy omissions but has exact-deficiency semantics:
- a new rule must satisfy the complete contract;
- a legacy rule may not acquire a new omission;
- fixing a legacy omission requires updating the baseline;
- deleting or renaming a baseline rule requires updating the baseline;
- duplicate alert names and unsupported severity/category values fail.
Run: