This exercise implements Stage 4 and Stage 5 of the capstone pipeline: quality monitoring with tiered alerting, SLA tracking, and OpenLineage lineage emission for all five pipeline stages. The monitoring layer sits across the full pipeline and fires notifications based on quality gate results, SLA breaches, and pipeline completion. The lineage layer emits START and COMPLETE events for each stage with structured input and output dataset references. The exercise ends with a full integration test that verifies all assertions across Stages 1–5.
The monitoring implementation uses the tiered alert router from Module 4 and the OpenLineage event emitter from Module 4 Lesson 22. All alert and lineage calls are wrapped in `try/except` blocks to ensure they are best-effort — pipeline failures are reported by the monitoring layer, not caused by it. The integration test asserts that all five pipeline stages have emitted lineage events with non-null job names, at least one input and one output, and that alert counts match expectations for each test scenario.
Step 1 — SLA Monitoring and Tiered Alerting
Implement the SLA monitor that tracks each pipeline stage's start time and fires a Slack warning if it exceeds the configured limit, the tiered alert router that sends critical quality failures to PagerDuty and warnings to Slack, and the end-of-run summary task. Run three test scenarios: a clean pipeline run (no alerts), a quality gate failure (PagerDuty critical), and an SLA breach (Slack warning). Assert the alert counts match expectations for each scenario.
# capstone_monitoring.py — Step 1: SLA monitoring and tiered alerting
from datetime import datetime, timezone, timedelta
import time
# ── Alert sinks ────────────────────────────────────────────────────────────────
pagerduty_alerts: list = []
slack_alerts: list = []
def mock_pagerduty(title: str, details: str) -> None:
pagerduty_alerts.append({"title": title, "details": details})
print(f" [PAGERDUTY] {title}")
def mock_slack(message: str, severity: str = "warning") -> None:
slack_alerts.append({"message": message, "severity": severity})
print(f" [SLACK/{severity.upper()}] {message}")
def tiered_alert(severity: str, title: str, details: str) -> None:
try:
if severity == "critical":
mock_pagerduty(title, details)
else:
mock_slack(details, severity)
except Exception as exc:
print(f" Alert failed (non-blocking): {exc}")
# ── SLA Monitor ────────────────────────────────────────────────────────────────
class PipelineSLAMonitor:
def __init__(self):
self.stage_starts: dict[str, datetime] = {}
self.stage_slas: dict[str, float] = {}
def start_stage(self, stage: str, sla_minutes: float) -> None:
self.stage_starts[stage] = datetime.now(timezone.utc)
self.stage_slas[stage] = sla_minutes
def check_stage(self, stage: str) -> dict:
if stage not in self.stage_starts:
return {"breach": False, "elapsed_min": 0}
elapsed = (
datetime.now(timezone.utc) - self.stage_starts[stage]
).total_seconds() / 60
breach = elapsed > self.stage_slas.get(stage, 60)
return {"stage": stage, "elapsed_min": round(elapsed, 3),
"sla_min": self.stage_slas.get(stage), "breach": breach}
# ── End-of-run summary ────────────────────────────────────────────────────────
def send_run_summary(
logical_date: str,
matches: int,
rows_loaded: int,
dlq_count: int,
ge_passed: bool,
sla_breach: bool,
) -> str:
status = "✅ SUCCESS" if ge_passed and not sla_breach else "⚠️ ISSUES"
message = (
f"{status} | IPL daily pipeline {logical_date}\n"
f" Matches: {matches} | Rows: {rows_loaded:,} | DLQ: {dlq_count}"
f"{' | ⚠️ SLA breached' if sla_breach else ''}"
)
mock_slack(message, severity="info" if ge_passed else "warning")
return message
# ── Test scenarios ─────────────────────────────────────────────────────────────
pagerduty_alerts.clear()
slack_alerts.clear()
sla_monitor = PipelineSLAMonitor()
# Scenario 1: clean run — only info summary
sla_monitor.start_stage("extract", sla_minutes=5)
sla_monitor.start_stage("quality", sla_minutes=2)
sla_monitor.start_stage("dbt_build",sla_minutes=15)
time.sleep(0.01) # simulate fast run
for stage in ["extract", "quality", "dbt_build"]:
check = sla_monitor.check_stage(stage)
if check["breach"]:
tiered_alert("warning", f"SLA breach: {stage}",
f"{stage} took {check['elapsed_min']}min (limit {check['sla_min']}min)")
summary1 = send_run_summary("2024-04-20", matches=3, rows_loaded=360,
dlq_count=0, ge_passed=True, sla_breach=False)
assert len(pagerduty_alerts) == 0
assert len(slack_alerts) == 1 # only summary
print(f" Scenario 1 (clean run): {len(slack_alerts)} Slack, {len(pagerduty_alerts)} PD ✓")
# Scenario 2: quality gate failure — PagerDuty critical
pagerduty_alerts.clear(); slack_alerts.clear()
tiered_alert("critical", "Quality gate FAILED",
"runs=9 detected in 1 row — delivery_id=9999")
assert len(pagerduty_alerts) == 1
print(f" Scenario 2 (quality fail): {len(pagerduty_alerts)} PD critical ✓")
# Scenario 3: SLA breach — Slack warning
pagerduty_alerts.clear(); slack_alerts.clear()
sla_breach_monitor = PipelineSLAMonitor()
sla_breach_monitor.start_stage("dbt_build", sla_minutes=0.0001) # instant breach
time.sleep(0.01)
check = sla_breach_monitor.check_stage("dbt_build")
assert check["breach"] == True
tiered_alert("warning", "SLA breach: dbt_build",
f"dbt_build took {check['elapsed_min']}min (limit {check['sla_min']}min)")
assert len(slack_alerts) == 1
print(f" Scenario 3 (SLA breach): {len(slack_alerts)} Slack warning ✓")
print("Step 1 ✓: SLA monitoring and tiered alerting verified")Step 2 — OpenLineage Emission and Integration Test
Emit OpenLineage START and COMPLETE events for all five pipeline stages with structured dataset references, then run the complete integration test. The integration test asserts: all five stages have emitted events, every event has a non-null job name and at least one input and one output dataset, the warehouse row count is stable (idempotency verified), the dbt mart row count conservation holds, and the alert counts match expectations for a clean run. A full summary is printed showing every assertion's status before the test is marked complete.
# capstone_monitoring.py — Step 2: Lineage emission and integration test
import uuid
from datetime import datetime, timezone
# ── OpenLineage event emitter (best-effort) ────────────────────────────────────
def emit_lineage(
job_name: str,
namespace: str,
event_type: str,
inputs: list[dict],
outputs: list[dict],
run_id: str = None,
) -> str:
run_id = run_id or str(uuid.uuid4())
event = {
"eventType": event_type,
"eventTime": datetime.now(timezone.utc).isoformat(),
"run": {"runId": run_id},
"job": {"namespace": namespace, "name": job_name},
"inputs": inputs,
"outputs": outputs,
}
try:
lineage_log.append(event)
# In production: requests.post(MARQUEZ_URL, json=event)
except Exception as exc:
print(f" Lineage failed (non-blocking): {exc}")
return run_id
def ds(namespace: str, name: str) -> dict:
return {"namespace": namespace, "name": name}
# ── Emit lineage for all five pipeline stages ─────────────────────────────────
lineage_log.clear()
stages = [
("ipl.wait_sensor",
[ds("s3", "ipl-raw/scorecards/2024-04-20/")],
[ds("airflow", "sensor_pass_token")]),
("ipl.extract_matches",
[ds("ipl-api", "matches")],
[ds("postgres", "staging.deliveries")]),
("ipl.quality_gate",
[ds("postgres", "staging.deliveries")],
[ds("postgres", "warehouse.validated_deliveries")]),
("ipl.dbt_build",
[ds("postgres", "warehouse.validated_deliveries")],
[ds("postgres", "analytics.fct_ipl_bowler_season_stats")]),
("ipl.monitor_alert",
[ds("postgres", "analytics.fct_ipl_bowler_season_stats")],
[ds("slack", "#ipl-data-alerts")]),
]
for job_name, inputs, outputs in stages:
rid = emit_lineage(job_name, "ipl-data-platform", "START", inputs, outputs)
emit_lineage(job_name, "ipl-data-platform", "COMPLETE", inputs, outputs, run_id=rid)
print(f" Lineage events emitted: {len(lineage_log)} ({len(stages)*2} expected)")
assert len(lineage_log) == len(stages) * 2
# ── Full integration test ────────────────────────────────────────────────────
print("\n=" * 55)
print("Capstone Integration Test — Course 4")
print("=" * 55)
assertions = []
# A1: All pipeline stages have lineage events
jobs_with_events = {e["job"]["name"] for e in lineage_log}
for job_name, _, _ in stages:
ok = job_name in jobs_with_events
assertions.append((f"Lineage: {job_name}", ok))
# A2: All lineage events have non-null job and ≥1 input and output
for event in lineage_log:
ok = (event["job"]["name"] is not None and
len(event["inputs"]) >= 1 and
len(event["outputs"]) >= 1)
assertions.append((f"Event structure: {event['job']['name']}/{event['eventType']}", ok))
# A3: Warehouse idempotency
wh_rows = len(warehouse_db.get("2024-04-20", []))
assertions.append(("Warehouse idempotency (360 rows)", wh_rows == 360))
# A4: dbt mart row conservation
mart_total = int(fct_after["deliveries"].sum())
all_rows = len(all_del)
assertions.append((f"dbt row conservation ({mart_total}=={all_rows})",
mart_total == all_rows))
# A5: Alerts on clean run (0 PD, 1 Slack info)
pagerduty_alerts.clear(); slack_alerts.clear()
send_run_summary("2024-04-20", 3, wh_rows, 0, True, False)
assertions.append(("Clean run: 0 PD alerts", len(pagerduty_alerts) == 0))
assertions.append(("Clean run: 1 Slack summary",len(slack_alerts) == 1))
# Print results
all_pass = True
for name, ok in assertions:
icon = "✓" if ok else "✗"
print(f" {icon} {name}")
if not ok: all_pass = False
print()
if all_pass:
print("ALL ASSERTIONS PASSED — Capstone pipeline is submission-ready ✓")
else:
failed = [n for n, ok in assertions if not ok]
raise AssertionError(f"FAILED: {failed}")Warning: The integration test asserts `mart_total == all_rows`, which requires the incremental simulation to have correctly merged all deliveries including the new batch. If the incremental watermark is set incorrectly — for example, using today's date instead of the epoch sentinel — the new deliveries may be excluded and `mart_total` will be less than `all_rows`, causing the conservation assertion to fail. Always initialise the incremental watermark with a past sentinel value (e.g. `1900-01-01`) to ensure the first run processes all available data, and use `COALESCE(MAX(loaded_at), '1900-01-01')` in the `is_incremental()` condition.
Extension Challenge: Add a lineage validation function that reads the `lineage_log` list and produces a simple lineage graph: for each dataset, which jobs produce it (outputs) and which jobs consume it (inputs). Print the graph in a readable format and verify that the `analytics.fct_ipl_bowler_season_stats` dataset is produced by `ipl.dbt_build` and consumed by `ipl.monitor_alert`. This simulates the Marquez graph traversal that data engineers use for impact analysis in production.