Observability & SLAs
Lesson 12 · Data Engineering with Python · ~15 min read
1. Why observability comes before optimization
A pipeline that runs silently is not a healthy pipeline — it is an unobserved one. Before you optimise query costs, evolve schemas, or add access controls, you need the answer to a simpler question: is the data in the table the right data, and is it there when it is supposed to be? Without observability you cannot answer either half.
Barr Moses’s “five pillars of data observability” framework (freshness, volume, schema, distribution, lineage) and the LinkedIn DataHub paper both converge on the same insight: the majority of production data incidents are not caused by job failures, which are loud and self-announcing, but by silent failures — jobs that succeed but load wrong, stale, or incomplete data. An Airflow task that turns green after writing zero rows is a silent failure.
Industry pull-quote worth remembering: “A pipeline is in production the day it has a freshness SLA — not the day it has a DAG.” The DAG tells the scheduler what to run. The SLA tells the data team what “healthy” means. You cannot have the second conversation without the first.
Observability is placed first in Module 5 because every subsequent lesson — schema evolution, cost attribution, access controls — relies on it. You cannot detect a schema change you did not instrument. You cannot attribute cost to a pipeline run you did not trace. Instrument first; optimise second.
2. The four pillars (freshness / volume / schema / distribution)
The four pillars that cover the vast majority of production data quality failures are freshness, volume, schema, and distribution. Each has a canonical numeric signal that can be checked automatically without human intervention.
Freshness measures how recent the newest data in a table is. The canonical signal
is now − max(created_at): the age of the most-recent event. For a table
receiving hourly data, an age of 3 hours is a breach; an age of 45 minutes is normal. Freshness is
the most actionable pillar because it has a natural threshold derived from the source’s
publish cadence. This lesson builds the freshness check end-to-end.
Volume measures whether the expected number of rows is arriving. The canonical signal is the row count delta: how many rows landed in the latest hour compared to the trailing 24-hour average. A drop to 10% of the average means either the source is down or the extract is broken. This is covered as Task 1 in the homework.
Schema measures whether the shape of the table has changed unexpectedly. The
canonical signal is a hash of the column set: frozenset(table.columns), serialised
and compared to the previous run. A mismatch means a new column appeared or an existing one was
renamed or dropped. This is the subject of Lesson 13.
Distribution measures whether values are in expected ranges. The canonical signals
are null rate (what fraction of a non-nullable column is null?) and top-N value share (if the
actor column’s top value accounts for 70% of all rows today when it normally
accounts for 0.5%, a bot run or a data-load error has changed the data). Distribution checks
belong to Great Expectations suites; they are set up in Lesson 03 and refined in Lesson 13.
3. OpenLineage as the standard event format
Before OpenLineage (openlineage.io, adopted by the Linux Foundation in 2021), lineage in a
multi-tool pipeline was assembled by hand: scrape Airflow’s task logs, parse dbt’s
manifest.json, query the data warehouse for table dependencies. The result was always
incomplete and always out of date.
OpenLineage solves this with a single event envelope that every tool emits in the same JSON shape. The envelope fields are:
eventType— one ofSTART,COMPLETE,FAIL,ABORT.eventTime— ISO 8601 UTC timestamp.producer— URI identifying the tool or pipeline that emitted the event.job—{"namespace": "...", "name": "..."}. Namespaces are typically pipeline names; job names are task names.run—{"runId": "<uuid>"}. Correlates START and COMPLETE events for the same task execution.inputs[]andoutputs[]— arrays of dataset references, each with namespace and name. These are the edges of the lineage graph.facets{}— optional extension dictionary for schema, column-level lineage, data quality assertions, and custom metadata.
A rendered START event from the sample pipeline extract task looks like this:
{
"eventType": "START",
"eventTime": "2026-05-26T10:00:00.000000+00:00",
"producer": "https://github.com/dmitry-kharytonov/claude-lessons/data-engineering",
"job": {
"namespace": "gharchive-pipeline",
"name": "gharchive_hourly.extract"
},
"run": {"runId": "4b3a2c1d-8e6f-4a1b-b2c3-d4e5f6a7b8c9"},
"inputs": [{"namespace": "gharchive-pipeline", "name": "raw_events"}],
"outputs": [{"namespace": "gharchive-pipeline", "name": "dim_events"}]
}
When the task completes, a matching COMPLETE event arrives with the same runId. Marquez
joins them by runId to build the run timeline. If a COMPLETE event never arrives (the
process was killed), the run is recorded as failed — which is exactly the right signal.
4. Marquez as the OpenLineage backend
Marquez (marquezproject.github.io) is the reference implementation of an OpenLineage backend: a
REST API that receives events, stores them in Postgres, and derives a dataset-and-job graph that
the web UI renders. It is open source, actively maintained, and small enough to run in a single
docker compose up on a laptop.
What Marquez stores: every event that arrives at its POST /api/v1/lineage endpoint,
plus the derived graph of jobs, runs, and datasets. The web UI at port 3000 renders three panels:
the Jobs panel (every emitting job), the Runs panel (the timeline for a selected job), and the
Datasets panel (the lineage graph). The Datasets graph is the key operational artefact: a directed
acyclic graph where a node is a table and an edge is a job that reads or writes it.
Why Marquez over a custom Postgres table? The free lineage UI; the OpenLineage-native storage schema (no ETL from events to graph); and the HTTP API that lets you query the graph programmatically (useful for impact analysis: “which downstream dashboards are affected if I rename this column?”). For a team that already runs DataHub, Atlas, or another metadata platform, OpenLineage events from the pipeline flow to whichever backend is already deployed.
5. Freshness SLAs in practice
An SLA is a commitment with a consequence. “The events table will not be older
than 90 minutes” is an SLA; “near real-time” is not. The first can be tested
automatically; the second cannot.
The formula for deriving a freshness SLA for a batch pipeline:
SLA = source publish cadence + pipeline run cadence + processing time + buffer
For the gharchive pipeline: source publishes hourly (60 min), DAG is scheduled hourly (60 min), extract + transform + load takes roughly 5 minutes, buffer for retries and late data is 25 minutes. Worst-case age = 150 minutes; SLA = 90 minutes (a breach at 90 minutes means you have at least 60 minutes to investigate before the worst case is reached).
One common pitfall: SLAs that apply only during business hours. A Sunday-morning gharchive outage that starts at 02:00 UTC and is not caught until Monday at 09:00 is a 31-hour data gap. If the SLA only fires alerts during business hours, on-call never sees it until after the data is seven hours old and stakeholders have already noticed. Freshness checks must run 24/7, unconditionally. Use a single SLA per critical table; do not sub-divide by partition or day-of-week. The uniformity makes the SLA predictable and its breach page-worthy rather than ignorable.
6. Alert design
Three rules for alerts that data teams actually act on:
Rule 1: every alert names a runbook URL. An alert without a runbook trains the on-call engineer to ignore it. A runbook does not need to be long — three bullet points (check the Airflow DAG, check the source, check the table row count) is enough. The discipline of writing the runbook before the alert fires is how you discover whether the alert is actionable.
Rule 2: alerts go to a channel a human reads. A #data-alerts Slack
channel that receives 40 alerts per day is not a channel anyone reads. Route SLA breach alerts
directly to the on-call rotation. Route volume-drop alerts to the data team’s primary channel.
An alert that fires into the void is not an alert; it is a log line.
Rule 3: freshness alerts must hold-down. If the data source publishes a batch five minutes late, the freshness check will fire once and then clear on the next run. Without a hold-down (a minimum breach duration before the alert is sent), a brief source delay pages someone every hour. A five-minute hold-down eliminates most transient false positives while still catching real outages within ten minutes.
7. Common pitfalls
Common pitfalls
- Treating “DAG succeeded” as “pipeline succeeded.” The load step can return zero rows and Airflow marks the task green. A green DAG is a necessary condition for a healthy pipeline, not a sufficient one. Always add a row-count assertion or a freshness check as the final task.
-
Setting freshness SLAs in adjectives instead of minutes. “Near
real-time,” “daily,” and “timely” cannot be automated. The
moment you cannot write
if age > N: alert(), the SLA is not operational. Always express freshness SLAs as a number of minutes. -
Emitting OpenLineage events from a
try/exceptthat swallows the failure event. A pattern liketry: do_work(); emit(COMPLETE) except: passmeans that whendo_work()raises, the COMPLETE event is never emitted and Marquez records the run as still in-flight indefinitely. The correct pattern: emit FAIL in the except block, then re-raise. Failed runs vanish from Marquez only when the FAIL event is swallowed. - Building dashboards no one looks at and forgetting alerts. Dashboards are forensics: you review them after an alert fires to understand what happened. They do not substitute for alerts. A beautiful Grafana dashboard that no one checks is not observability; it is decoration. Instrument alerts first; add dashboards to help explain breaches.
References
- OpenLineage specification — openlineage.io/spec
- Marquez documentation — marquezproject.ai/docs
- Moses, B. & Stanford, J. (2022). Data Quality Fundamentals (O’Reilly). Chapter 3: Freshness and Volume. The canonical reference for the “five pillars” framing used throughout this lesson.
- Apache Airflow OpenLineage provider documentation — airflow.apache.org/docs/apache-airflow-providers-openlineage
- L11 (data-engineering/lesson11-cloud-warehouse/theory.html) — cloud warehouse and Iceberg setup that L12 instruments.
- L13 (data-engineering/lesson13-schema-evolution-and-lineage/theory.html) — schema evolution, data contracts, and full end-to-end lineage.