| name | dataops-sla-monitoring |
| description | Data platform SLA monitoring — freshness SLAs (data available by X:XX), completeness SLAs (row count variance < N%), latency SLAs (pipeline completes within N minutes), SLA breach detection (Prometheus/Soda/SQL), SLA burn rate alerting, SLA reporting dashboards, SLA miss root cause linking, cascading SLA dependencies (upstream delay propagation), SLA definition process, error budget tracking, consumer notification on SLA breach |
DataOps SLA Monitoring
When to Use
- Defining SLAs for data products consumed by BI/analytics
- Setting up freshness and completeness monitoring
- Alerting consumers when a data SLA is at risk
- Tracking error budget consumption across pipelines
- Investigating an SLA miss root cause
SLA Definition Framework
version: "1.0"
slas:
- name: orders_daily_freshness
description: "Daily orders data must be available by 06:00 UTC"
dataset: gold.fact_orders
type: freshness
target:
available_by: "06:00 UTC"
timezone: UTC
breach_threshold_minutes: 60
owner_team: data-engineering
consumer_teams: [analytics, bi-team, finance]
severity: high
error_budget_minutes_per_month: 120
- name: orders_completeness
description: "Daily orders row count within 1% of previous week's same-day"
dataset: gold.fact_orders
type: completeness
target:
max_row_count_variance_pct: 1.0
lookback_days: 7
owner_team: data-engineering
severity: medium
- name: kafka_consumer_latency
description: "Orders topic consumer lag < 10,000 messages at all times"
type: streaming_latency
target:
max_lag: 10000
consumer_group: orders-processor
severity: high
Freshness SLA Monitoring
Prometheus-Based
groups:
- name: sla_recordings
rules:
- record: pipeline_last_success_timestamp
expr: |
max by (dag_id) (
airflow_dagrun_end_date{state="success"}
)
- record: pipeline_freshness_seconds
expr: |
time() - pipeline_last_success_timestamp
- name: sla_alerts
rules:
- alert: DataFreshnessAtRisk
expr: |
pipeline_freshness_seconds{dag_id="etl_orders"} > 10800 # 3h = at risk
for: 5m
labels:
severity: warning
sla: orders_daily_freshness
annotations:
summary: "Orders data freshness at risk"
description: "Last successful run was {{ $value | humanizeDuration }} ago (SLA: 6h)"
- alert: DataFreshnessSLABreached
expr: |
pipeline_freshness_seconds{dag_id="etl_orders"} > 21600 # 6h = breached
labels:
severity: critical
sla: orders_daily_freshness
SQL-Based Freshness Check
WITH sla_definitions AS (
SELECT 'gold.fact_orders' AS table_name,
360 AS max_age_minutes,
'orders_daily_freshness' AS sla_name
),
table_freshness AS (
SELECT
'gold.fact_orders' AS table_name,
MAX(updated_at) AS last_update,
TIMESTAMPDIFF(MINUTE, MAX(updated_at), NOW()) AS age_minutes
FROM gold.fact_orders
WHERE order_date = CURRENT_DATE
)
SELECT
d.sla_name,
d.table_name,
f.last_update,
f.age_minutes,
d.max_age_minutes,
CASE
WHEN f.age_minutes IS NULL THEN 'NO_DATA'
WHEN f.age_minutes > d.max_age_minutes THEN 'BREACHED'
WHEN f.age_minutes > d.max_age_minutes * 0.8 THEN 'AT_RISK'
ELSE 'OK'
END AS sla_status
FROM sla_definitions d
LEFT JOIN table_freshness f USING (table_name);
Completeness SLA
checks for gold.fact_orders:
- row_count:
name: "Daily orders completeness SLA"
fail:
when not between:
min_percentage: -1.0
max_percentage: 200.0
previous_period: 7d
group_by: order_date
- anomaly detection for row_count:
name: "Anomaly detection: orders volume"
warn:
when > 3 stdevs
import pendulum
from sqlalchemy import create_engine
def check_completeness_sla(partition_date: str) -> dict:
engine = create_engine(TRINO_URI)
last_week = pendulum.parse(partition_date).subtract(days=7).date()
current_count = engine.execute(
f"SELECT COUNT(*) FROM gold.fact_orders WHERE order_date = '{partition_date}'"
).scalar()
reference_count = engine.execute(
f"SELECT COUNT(*) FROM gold.fact_orders WHERE order_date = '{last_week}'"
).scalar()
if reference_count == 0:
return {"status": "NO_REFERENCE", "current": current_count}
variance_pct = abs(current_count - reference_count) / reference_count * 100
return {
"status": "BREACHED" if variance_pct > 1.0 else "OK",
"current_count": current_count,
"reference_count": reference_count,
"variance_pct": round(variance_pct, 2),
"partition_date": partition_date,
}
Error Budget Tracking
from datetime import datetime, timedelta
import psycopg2
def calculate_error_budget(sla_name: str, month: datetime) -> dict:
"""Calculate error budget for a given SLA in a calendar month."""
target_pct = 99.5
month_minutes = 30 * 24 * 60
budget_minutes = month_minutes * (1 - target_pct / 100)
conn = psycopg2.connect(METADATA_DB)
actual_downtime = conn.execute("""
SELECT COALESCE(SUM(duration_minutes), 0) AS total_breach_minutes
FROM sla_breach_log
WHERE sla_name = %s
AND breach_start >= %s
AND breach_start < %s
""", (sla_name, month.replace(day=1), (month + timedelta(days=31)).replace(day=1))).fetchone()[0]
remaining_budget = budget_minutes - actual_downtime
budget_consumed_pct = actual_downtime / budget_minutes * 100
return {
"sla_name": sla_name,
"month": month.strftime("%Y-%m"),
"budget_minutes": budget_minutes,
"consumed_minutes": actual_downtime,
"remaining_minutes": remaining_budget,
"budget_consumed_pct": round(budget_consumed_pct, 1),
"status": "EXHAUSTED" if remaining_budget <= 0 else
"AT_RISK" if budget_consumed_pct > 50 else "OK",
}
Consumer Notification on SLA Breach
def sla_miss_callback(dag, task_list, blocking_task_list, slas, blocking_tis):
import requests
consumers = get_sla_consumers(dag.dag_id)
message = {
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": f"⚠️ Data SLA Breach: {dag.dag_id}"}
},
{
"type": "section",
"text": {"type": "mrkdwn", "text":
f"*DAG:* `{dag.dag_id}`\n"
f"*Blocked tasks:* {[t.task_id for t in blocking_task_list]}\n"
f"*SLA:* Data should have been available by 06:00 UTC\n"
f"*Impact:* Affects dashboards consumed by {', '.join(consumers)}"
}
},
{
"type": "actions",
"elements": [{
"type": "button",
"text": {"type": "plain_text", "text": "View in Airflow"},
"url": f"http://airflow.internal/dags/{dag.dag_id}/grid",
}]
}
]
}
for channel in ["#data-eng-sla", "#analytics-alerts", "#bi-team"]:
requests.post(SLACK_WEBHOOK, json={"channel": channel, **message})
SLA Reporting Dashboard (SQL)
SELECT
sla_name,
COUNT(*) AS total_checks,
SUM(CASE WHEN status = 'OK' THEN 1 ELSE 0 END) AS ok_count,
SUM(CASE WHEN status = 'BREACHED' THEN 1 ELSE 0 END) AS breach_count,
ROUND(100.0 * SUM(CASE WHEN status = 'OK' THEN 1 ELSE 0 END) / COUNT(*), 2) AS success_rate_pct,
ROUND(AVG(CASE WHEN status = 'BREACHED' THEN breach_duration_minutes END), 1) AS avg_breach_min,
MAX(CASE WHEN status = 'BREACHED' THEN breach_duration_minutes END) AS max_breach_min
FROM sla_check_log
WHERE check_date >= CURRENT_DATE - INTERVAL '7' DAY
GROUP BY sla_name
ORDER BY success_rate_pct;
Anti-Patterns
- No SLA defined = no breach possible — teams learn about stale data from angry Slack messages; define SLAs before deploying pipelines.
- SLA threshold = pipeline max runtime — SLA should be defined from the consumer's perspective (data available by X time), not pipeline runtime.
- Alerting only the data engineering team — consumers don't know data is late and keep using stale numbers; notify consumer teams too.
- Error budget not tracked — teams keep burning budget without realizing it; track and report monthly budget consumption.
- Freshness measured at pipeline completion — the pipeline might complete but load 0 rows; always check actual data recency, not just pipeline status.
References
- Google SRE: SLOs:
sre.google/sre-book/service-level-objectives/
- Soda SodaCL freshness:
docs.soda.io/soda-cl/freshness.html
- Related skills:
[[dataops-airflow-production-readiness]], [[infra-alert-fatigue-reduction]], [[de-production-readiness]], [[soda-core]]