Alerting on Failed Tile Jobs
Evaluate a few rules against the run manifest after every run and send one message if any fail:
def check(manifest, history):
alerts = []
failed = len(manifest["tiles"]["failed"])
if failed > max(5, 0.005 * manifest["tiles"]["attempted"]):
alerts.append(f"{failed} tiles failed")
if manifest["wall_s"] > 2 * median(h["wall_s"] for h in history):
alerts.append("runtime more than 2x recent median")
return alerts
An alert is a request for someone’s attention, so every rule should describe a situation where a person needs to act. This page belongs to monitoring and observability for raster pipelines in Cloud Execution & Orchestration.
Four Rules That Cover Most Failures
The missing-run rule is the one most often forgotten and the one that catches the worst failures. When a scheduler stops, credentials expire before start-up, or a deployment breaks the entry point, nothing runs and therefore nothing logs an error. Only an external check that expects a manifest by a given time notices the silence — a “dead man’s switch”.
Environment & Setup
| Package | Version pin | Used for |
|---|---|---|
httpx |
>=0.26 |
Posting to a Slack webhook |
boto3 |
>=1.34 |
Reading manifests from S3 |
prefect |
>=2.14 |
Optional: automations on flow state |
pip install "httpx>=0.26" "boto3>=1.34" "prefect>=2.14"
Complete Working Example
import json
import os
import statistics
from dataclasses import dataclass
import httpx
@dataclass
class Rules:
max_failed_abs: int = 5
max_failed_frac: float = 0.005
max_empty_tiles: int = 0
regression_factor: float = 2.0
def evaluate(m: dict, history: list[dict], r: Rules = Rules()) -> list[str]:
out = []
t = m["tiles"]
failed = t["failed"]
if len(failed) > max(r.max_failed_abs, r.max_failed_frac * t["attempted"]):
out.append(f"{len(failed)}/{t['attempted']} tiles failed, e.g. {', '.join(failed[:5])}")
empty = m.get("quality", {}).get("empty_tiles", [])
if len(empty) > r.max_empty_tiles:
out.append(f"{len(empty)} tiles written as all nodata, e.g. {', '.join(empty[:5])}")
if len(history) >= 3:
for key in ("wall_s", "bytes_read", "cost_usd"):
base = statistics.median(h[key] for h in history if key in h)
if base and m.get(key, 0) > r.regression_factor * base:
out.append(f"{key} {m[key]:.3g} vs recent median {base:.3g}")
return out
def notify(run_id: str, alerts: list[str], webhook: str | None = None) -> None:
if not alerts:
return
text = f":warning: raster pipeline run {run_id}\n" + "\n".join(f"• {a}" for a in alerts)
httpx.post(webhook or os.environ["SLACK_WEBHOOK_URL"], json={"text": text}, timeout=10)
def after_run(run_id: str, manifest: dict, history: list[dict]) -> list[str]:
alerts = evaluate(manifest, history)
notify(run_id, alerts)
return alerts
The function sends at most one message per run, listing every failing rule with a few example tiles. Grouping matters: a hundred individual messages for a hundred failed tiles are read once and then muted, while one message saying “100 of 8,000 tiles failed, e.g. …” is read every time. The manifest itself comes from the run summary described in monitoring and observability for raster pipelines.
Keeping Alerts Worth Reading
Every alert that turns out to need no action erodes trust in the channel. Three habits keep the signal clean. First, fix the causes of recurring alerts rather than raising thresholds: tiles that always fail because they lie over open ocean belong in an explicit skip list recorded in the manifest, not in a tolerance. Second, separate urgency: a failed run that blocks a morning product goes to a pager or direct message, a runtime regression goes to a team channel for daytime review. Third, include enough in the message to start work — run identifier, example tiles and a link to the logs or performance report — so the first response is investigation, not searching.
The Dead Man’s Switch
Rules evaluated at the end of a run cannot notice runs that never ended. A separate scheduled check — a small cloud function or a cron job on different infrastructure — should look for the expected manifest and alert if it is missing or stale:
import datetime as dt
import boto3
def check_heartbeat(bucket: str, prefix: str, max_age_h: float = 26) -> list[str]:
s3 = boto3.client("s3")
objs = s3.list_objects_v2(Bucket=bucket, Prefix=prefix).get("Contents", [])
if not objs:
return ["no manifests found"]
newest = max(o["LastModified"] for o in objs)
age = (dt.datetime.now(dt.timezone.utc) - newest).total_seconds() / 3600
return [f"latest manifest is {age:.0f} h old"] if age > max_age_h else []
Running it somewhere other than the pipeline’s own scheduler is the point: if the scheduler is what broke, a check that depends on it breaks too. Hosted heartbeat services that expect a ping and alert on its absence achieve the same with less code.
Alerts from the Orchestrator
Orchestrators provide their own alerting for task and run state, and it is worth using as a backstop. Prefect automations can notify on any flow run entering a failed or crashed state; AWS Batch job state changes are published to EventBridge, where a rule can forward failed jobs to SNS. These catch crashes that happen before the pipeline’s own code can write a manifest. They do not catch quiet failures — a successful run that wrote empty tiles — so they complement the manifest rules rather than replacing them. The retry policies in retrying failed raster tasks in a Prefect pipeline decide which failures reach this point at all.
Verification
m = {"tiles": {"attempted": 1000, "failed": [f"t{i}" for i in range(12)]},
"quality": {"empty_tiles": ["t99"]}, "wall_s": 9000}
hist = [{"wall_s": 3000}] * 5
alerts = evaluate(m, hist)
assert any("tiles failed" in a for a in alerts)
assert any("nodata" in a for a in alerts)
assert any("wall_s" in a for a in alerts)
Common Errors
Nobody noticed the pipeline stopped for a week
There was no missing-run check. Add a heartbeat evaluated on separate infrastructure.
The channel is muted by everyone
Too many alerts need no action. Fix recurring causes and move low-urgency rules to a digest.
Alerts contain presigned URLs
Messages were built from raw log lines. Include identifiers and dashboard links, never credentials.
Regression rule fires after every intentional change
The history includes runs with a different configuration. Reset the baseline when the pipeline version changes.
Frequently Asked Questions
Q: What should trigger an alert in a raster pipeline? Failed tiles above a small threshold, a run that never produced a manifest, tiles written empty or with impossible values, and runtime or cost more than about twice the recent median.
Q: How do I detect a pipeline that did not run at all? With a heartbeat check on separate infrastructure that expects a fresh manifest by a set time and alerts when it is missing.
Q: Should every failed tile send a notification? No. Group failures into one message per run with counts and examples. Individual messages per tile lead to muted channels.
Q: Do orchestrator alerts replace custom rules? No. They catch crashed runs, but not runs that succeed while producing bad data. Use both.
Related
- Monitoring and Observability for Raster Pipelines — the parent topic.
- Logging Structured Events from Raster Tasks — the events the manifest is built from.
- Retrying Failed Raster Tasks in a Prefect Pipeline — reducing what reaches the alert.
- Building a Prefect Deployment for Nightly Composites — a nightly run to alert on.