Prometheus Alerting Rules for Tile Availability

This procedure builds the multi-window, multi-burn-rate Prometheus rules that turn the raw 200-response rate of an OGC API Tiles or MVT endpoint into a paging-grade availability SLO, with recording rules that keep the alert expressions cheap and a promtool gate that proves they fire before they ship. It is the concrete rule set behind the availability objective defined in SLA Monitoring for Spatial Data Products, under the broader Spatial Pipeline Orchestration & Observability practice, and it pairs naturally with Monitoring Vector Query p95 Latency so a domain covers both the availability and the latency halves of its tile contract. Get the burn-rate windows right and a genuine outage pages within minutes while a deploy blip stays silent.

Prerequisites

Requirement Value / Assumption Notes
Prometheus >= 2.45 with rule evaluation enabled Recording + alerting rule groups
Validation promtool (ships with Prometheus) check rules and test rules
Tile metric tile_requests_total{code,domain,layer} counter Emitted by the tile server exporter
CRS labels EPSG:4326 and EPSG:3857 layers labelled distinctly Isolates reprojection-path faults
SLO target 0.995 availability over 30d Error budget 0.005
Alert routing Alertmanager receiver keyed on severity, domain page vs ticket split
Access role prometheus-rules-editor (RBAC) Required to apply rule files
Scrape security mTLS to exporter targets Zero-trust scrape path

Figure — Two burn-rate windows must both breach before a page fires; a slow single-window breach opens a ticket instead.

Multi-window multi-burn-rate alert decision The availability error rate is evaluated over a fast 5-minute window and a confirming 1-hour window. When both exceed 14.4 times the budget the AND gate fires a page. A separate 6-hour window exceeding 1 times the budget opens a ticket without paging. 5m window error > 14.4 × budget 1h window error > 14.4 × budget AND both breaching PAGE on-call severity: page 6h window error > 1 × budget TICKET severity: ticket

Step-by-Step Implementation

Each step below produces a fragment of the rule file and a command that proves the fragment is correct before you build on it.

1. Record the availability ratio at two windows

The alert expressions stay cheap and readable only if the SLI ratio is pre-computed. Record the 200-rate over both the fast and the confirming window, grouped by domain and layer so a single hot layer can be isolated.

yaml
# tile-availability-rules.yaml
groups:
  - name: tile_availability_recording
    interval: 30s
    rules:
      - record: tile:availability:ratio_rate5m
        expr: |
          sum(rate(tile_requests_total{code="200"}[5m])) by (domain, layer)
          /
          sum(rate(tile_requests_total[5m])) by (domain, layer)
      - record: tile:availability:ratio_rate1h
        expr: |
          sum(rate(tile_requests_total{code="200"}[1h])) by (domain, layer)
          /
          sum(rate(tile_requests_total[1h])) by (domain, layer)

Verify: confirm the rule file parses and the recording rules are well-formed.

bash
promtool check rules tile-availability-rules.yaml

2. Add the fast-burn page rule

The page fires only when the 5-minute and 1-hour windows both breach 14.4 times the 0.005 budget, which suppresses single-scrape spikes while still catching a fast burn within minutes.

yaml
  - name: tile_availability_alerts
    rules:
      - alert: TileAvailabilityFastBurn
        expr: |
          (1 - tile:availability:ratio_rate5m) > (14.4 * 0.005)
          and
          (1 - tile:availability:ratio_rate1h) > (14.4 * 0.005)
        for: 2m
        labels:
          severity: page
          slo: tile_availability
        annotations:
          summary: "Fast burn on {{ $labels.domain }}/{{ $labels.layer }} tiles"
          description: "Tile 200-rate breaching 99.5% budget at >14.4x."

Verify: re-check the combined file after adding the alert group.

bash
promtool check rules tile-availability-rules.yaml

3. Add the slow-burn ticket rule

A slow leak that would still exhaust the budget over the window should open a ticket, not wake anyone. Use a wider window at a burn rate of 1.

yaml
      - alert: TileAvailabilitySlowBurn
        expr: |
          (1 - avg_over_time(tile:availability:ratio_rate1h[6h])) > (1 * 0.005)
        for: 15m
        labels:
          severity: ticket
          slo: tile_availability
        annotations:
          summary: "Slow burn on {{ $labels.domain }}/{{ $labels.layer }} tiles"

Verify: confirm both alerts are registered in the group.

bash
promtool check rules tile-availability-rules.yaml | grep -E 'SUCCESS|FAILED'

4. Prove the rules fire with a unit test

A rule that parses but never evaluates true is worse than no rule. Drive a synthetic series through promtool test rules to assert the page fires on a crafted outage and the slow burn tickets on a steady leak.

yaml
# tile-availability-tests.yaml
rule_files:
  - tile-availability-rules.yaml
evaluation_interval: 30s
tests:
  - interval: 30s
    input_series:
      - series: 'tile_requests_total{code="200",domain="basemap",layer="roads"}'
        values: '0+5x120'          # steady successes then...
      - series: 'tile_requests_total{code="503",domain="basemap",layer="roads"}'
        values: '0+95x120'         # ...a heavy 503 flood
    alert_rule_test:
      - eval_time: 10m
        alertname: TileAvailabilityFastBurn
        exp_alerts:
          - exp_labels:
              severity: page
              slo: tile_availability
              domain: basemap
              layer: roads

Verify: run the behavioural test; a non-zero exit fails CI.

bash
promtool test rules tile-availability-tests.yaml

5. Apply and confirm the rules are live

Load the file into Prometheus and confirm the alert is registered and evaluating, not just present on disk.

bash
# after reload (SIGHUP or /-/reload), confirm the alert is loaded
curl -s http://localhost:9090/api/v1/rules \
  | jq '.data.groups[].rules[] | select(.name=="TileAvailabilityFastBurn") | {name, state}'

Configuration Reference

Field Scope Required value Effect
interval recording group 30s Evaluation cadence for the SLI ratios
[5m] fast window fast-burn expr 5m Detection window for a rapid outage
[1h] long window fast-burn expr 1h Confirmation leg suppressing deploy spikes
14.4 burn factor fast-burn expr 14.4 Pages when budget drains ~30x too fast
[6h] slow window slow-burn expr 6h Detection window for a slow leak
1 burn factor slow-burn expr 1 Tickets when budget is on pace to exhaust
0.005 budget both exprs 1 - objective Derived from the 0.995 SLO
for both alerts 2m / 15m Debounce before firing
severity alert labels page / ticket Alertmanager routing key

Common Failure Modes & Fixes

The page never fires during a real outage. Root cause: only the fast window is breaching because the outage is shorter than the 1-hour confirmation leg, or the by (domain, layer) grouping in the alert does not match the recorded series labels. Fix: confirm the recording rule and alert share identical grouping, and verify with promtool test rules using a fixture that spans both windows.

The page fires on every deploy. Root cause: the alert is watching only the 5-minute window without the 1-hour and leg, so a brief restart blip trips it. Fix: restore the two-window and condition and keep for: 2m so a single scrape cannot page.

No series returned from the recording rule. Root cause: the exporter emits status rather than code, or omits the domain label the rule groups by. Fix: align the selector to the exporter’s actual label set — promtool check metrics against a scrape sample reveals the true names.

Alert fires but never routes to on-call. Root cause: the severity: page label is present but Alertmanager’s route tree matches on a different key. Fix: confirm the receiver’s match block keys on severity and domain, and reload Alertmanager after the change.

Cardinality explosion stalls rule evaluation. Root cause: a raw z/x/y tile-coordinate label multiplies series into the millions. Fix: drop the per-tile label in the recording rule’s by () clause and keep tile identity in a separate, sampled diagnostic metric.

FAQ

Why 14.4 as the fast-burn factor?

With a 0.995 objective over 30 days, the total error budget is 0.005 of requests. A burn rate of 14.4 means the budget is being consumed 14.4 times faster than the window can absorb, which would exhaust the entire 30-day budget in about two days. That pace is unambiguously an incident, so it warrants a page rather than a ticket. Pairing it with the 1-hour confirmation window is what stops a momentary spike from reaching the same conclusion.

Should I alert per layer or per domain?

Both, at different severities. Group the recording rules by domain and layer so a single hot EPSG:3857 basemap layer timing out is visible, but consider routing the page on domain so on-call is not fragmented across dozens of layer-scoped alerts. The layer label stays on the alert as context for the responder without splitting the rotation.

How do I keep freshness separate from availability here?

This rule set measures only the 200-rate, which a stale-but-serving endpoint passes. Add a distinct freshness indicator — for example an age gauge compared against freshness.max_age_seconds — as its own recording rule and alert, so a green availability SLI and a red freshness SLI point unambiguously at the production pipeline rather than the tile server.

Can I test the rules without a live Prometheus?

Yes, and you should gate CI on it. promtool test rules evaluates the rule file against an inline synthetic series with no server running, so a crafted outage fixture proves the page fires and a steady-leak fixture proves the ticket fires. A rule change that breaks either assertion fails the build before it can reach the estate.