Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/ infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk (fra første ## seksjon), advisor urørt (0 endringer). To applier-fixes oppdaget under kjøring (TDD, RED→GREEN): - insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer 500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor scan-vinduet, applierens post-write-assertion fanget + restaurerte). - normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i 500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent utvidelse av carve-out; kun stray metadata-linjer, aldri prosa. test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet (fila bærer nå Source). Suite 728/728 grønn.
460 lines
17 KiB
Markdown
460 lines
17 KiB
Markdown
# Data Versioning and Lineage Tracking
|
|
|
|
**Last updated:** 2026-06-24
|
|
**Status:** GA
|
|
**Category:** Data Engineering for AI
|
|
**Type:** reference
|
|
**Source:** https://learn.microsoft.com/fabric/governance/lineage
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Delta Lake Versioning and Time-Travel](#delta-lake-versioning-and-time-travel)
|
|
- [Commit History and Audit Trails](#commit-history-and-audit-trails)
|
|
- [Data Lineage Visualization in Purview](#data-lineage-visualization-in-purview)
|
|
- [Upstream/Downstream Dependency Mapping](#upstreamdownstream-dependency-mapping)
|
|
- [Rollback and Recovery Strategies](#rollback-and-recovery-strategies)
|
|
- [Referanser](#referanser)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## Introduksjon
|
|
|
|
Dataversionskontroll og lineage-sporing er grunnleggende kapabiliteter for pålitelige AI-systemer. Versjonskontroll gjør det mulig å reprodusere eksakt de dataene en modell ble trent på, mens lineage dokumenterer hele datareisen fra kilde til ferdig prediksjon. Sammen gir de sporbarhet, reproduserbarhet og tillitsgrunnlag for AI-beslutninger.
|
|
|
|
For norsk offentlig sektor er dette spesielt viktig gitt kravene i Utredningsinstruksen om etterprøvbarhet, Forvaltningslovens krav til dokumentasjon av vedtak, og EU AI Act sine krav til høyrisiko AI-systemer. En modell som påvirker borgeres rettigheter -- for eksempel NAV-ytelser eller byggetillatelser -- må kunne forklares og dokumenteres fra kilde til prediksjon.
|
|
|
|
Denne referansen dekker Delta Lake versjonskontroll og tidsreise, commit-historikk og audit trails, lineage-visualisering i Purview og Fabric, avhengighetskartlegging, og strategier for rollback og gjenoppretting.
|
|
|
|
---
|
|
|
|
## Delta Lake Versioning and Time-Travel
|
|
|
|
### Versjonskontroll-modell
|
|
|
|
Delta Lake bruker en Write-Ahead Log (WAL) i `_delta_log`-mappen som registrerer alle transaksjoner:
|
|
|
|
```
|
|
Tables/ml_training_data/
|
|
├── _delta_log/
|
|
│ ├── 00000000000000000000.json # v0: Initial create (2026-01-01)
|
|
│ ├── 00000000000000000001.json # v1: Append new data (2026-01-15)
|
|
│ ├── 00000000000000000002.json # v2: Feature update (2026-02-01)
|
|
│ ├── 00000000000000000003.json # v3: Delete PII (2026-02-05)
|
|
│ └── 00000000000000000004.json # v4: Schema evolution (2026-02-10)
|
|
├── part-00000-*.snappy.parquet
|
|
├── part-00001-*.snappy.parquet
|
|
└── ...
|
|
```
|
|
|
|
### Versjonsspørringer
|
|
|
|
```python
|
|
from delta.tables import DeltaTable
|
|
|
|
# Les nåværende versjon
|
|
df_current = spark.read.format("delta").table("gold.ml_training_data")
|
|
|
|
# Les spesifikk versjon
|
|
df_v2 = spark.read.format("delta") \
|
|
.option("versionAsOf", 2) \
|
|
.table("gold.ml_training_data")
|
|
|
|
# Les data slik de var på et tidspunkt
|
|
df_jan = spark.read.format("delta") \
|
|
.option("timestampAsOf", "2026-01-15T00:00:00Z") \
|
|
.table("gold.ml_training_data")
|
|
|
|
# Sammenlign versjoner for å oppdage endringer
|
|
from pyspark.sql.functions import col
|
|
|
|
added_rows = df_v2.subtract(df_v1) # Rader i v2 som ikke finnes i v1
|
|
removed_rows = df_v1.subtract(df_v2) # Rader i v1 som ikke finnes i v2
|
|
|
|
print(f"Nye rader: {added_rows.count()}")
|
|
print(f"Fjernede rader: {removed_rows.count()}")
|
|
```
|
|
|
|
### Versjonskontroll for ML-eksperimenter
|
|
|
|
```python
|
|
import mlflow
|
|
|
|
# Logg data-versjon som del av ML-eksperiment
|
|
with mlflow.start_run(run_name="churn_model_v3"):
|
|
# Hent Delta-tabell-versjon
|
|
dt = DeltaTable.forPath(spark, "Tables/gold/ml_training_data")
|
|
current_version = dt.history(1).select("version").collect()[0][0]
|
|
|
|
# Logg metadata
|
|
mlflow.log_param("data_table", "gold.ml_training_data")
|
|
mlflow.log_param("data_version", current_version)
|
|
mlflow.log_param("data_timestamp", "2026-02-10T00:00:00Z")
|
|
mlflow.log_param("row_count", df_current.count())
|
|
mlflow.log_param("column_count", len(df_current.columns))
|
|
|
|
# Tren modell...
|
|
# mlflow.sklearn.log_model(model, "model")
|
|
|
|
# Senere: Reproduser treningsdata eksakt
|
|
# df_reproduced = spark.read.format("delta")
|
|
# .option("versionAsOf", logged_version)
|
|
# .table("gold.ml_training_data")
|
|
```
|
|
|
|
---
|
|
|
|
## Commit History and Audit Trails
|
|
|
|
### DESCRIBE HISTORY
|
|
|
|
```sql
|
|
-- Vis full transaksjonshistorikk
|
|
DESCRIBE HISTORY gold.ml_training_data;
|
|
|
|
-- Resultat:
|
|
-- version | timestamp | operation | operationParameters | operationMetrics
|
|
-- 4 | 2026-02-10 14:30:00 | WRITE | {mode: Append} | {numFiles: 3, numOutputRows: 15000}
|
|
-- 3 | 2026-02-05 09:15:00 | DELETE | {predicate: [pii_flag=1]} | {numDeletedRows: 250, numRemovedFiles: 2}
|
|
-- 2 | 2026-02-01 02:00:00 | MERGE | {predicate: ...} | {numUpdatedRows: 3400, numInsertedRows: 1200}
|
|
-- 1 | 2026-01-15 02:00:00 | WRITE | {mode: Append} | {numFiles: 5, numOutputRows: 50000}
|
|
-- 0 | 2026-01-01 10:00:00 | CREATE | {partitionBy: [date]} | {numFiles: 10, numOutputRows: 100000}
|
|
```
|
|
|
|
### PySpark History API
|
|
|
|
```python
|
|
from delta.tables import DeltaTable
|
|
|
|
dt = DeltaTable.forPath(spark, "Tables/gold/ml_training_data")
|
|
|
|
# Hent historikk
|
|
history = dt.history()
|
|
|
|
# Detaljert analyse av endringer
|
|
display(
|
|
history.select(
|
|
"version",
|
|
"timestamp",
|
|
"operation",
|
|
"operationParameters",
|
|
"operationMetrics",
|
|
"userName",
|
|
"notebook.notebookId"
|
|
).orderBy("version", ascending=False)
|
|
)
|
|
|
|
# Filtrer på spesifikke operasjoner
|
|
deletes = history.filter("operation = 'DELETE'")
|
|
merges = history.filter("operation = 'MERGE'")
|
|
```
|
|
|
|
### Custom Audit Logging
|
|
|
|
```python
|
|
from pyspark.sql import functions as F
|
|
from datetime import datetime
|
|
|
|
def log_data_operation(operation, table_name, details, user="system"):
|
|
"""Logg datapipelineoperasjoner til audit-tabell."""
|
|
audit_record = spark.createDataFrame([{
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"operation": operation,
|
|
"table_name": table_name,
|
|
"user": user,
|
|
"details": str(details),
|
|
"pipeline_run_id": spark.conf.get("spark.pipeline.runId", "interactive")
|
|
}])
|
|
|
|
audit_record.write.format("delta") \
|
|
.mode("append") \
|
|
.saveAsTable("governance.data_audit_log")
|
|
|
|
# Bruk i pipeline
|
|
log_data_operation(
|
|
operation="FEATURE_UPDATE",
|
|
table_name="gold.ml_training_data",
|
|
details={
|
|
"source_version": 3,
|
|
"target_version": 4,
|
|
"rows_added": 15000,
|
|
"rows_updated": 3400,
|
|
"features_modified": ["txn_7d_count", "income_band"]
|
|
}
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Data Lineage Visualization in Purview
|
|
|
|
### Lineage-kilder i Purview
|
|
|
|
Purview fanger automatisk lineage fra flere kilder:
|
|
|
|
| Dataprosesseringssystem | Lineage-omfang |
|
|
|---|---|
|
|
| **Azure Data Factory** | Copy Activity, Data Flow, SSIS |
|
|
| **Fabric Data Factory** | Pipelines, Dataflow Gen2 |
|
|
| **Fabric Notebooks** | Lakehouse → Lakehouse (item-level) |
|
|
| **Azure Synapse Analytics** | Copy Activity, Data Flow |
|
|
| **Power BI** | Semantic Model → Report → Dashboard |
|
|
|
|
### Lineage-visning i Fabric
|
|
|
|
```
|
|
Lineage-visning for en ML-pipeline:
|
|
|
|
Azure SQL DB Lakehouse (Bronze) Lakehouse (Silver)
|
|
┌──────────┐ ┌──────────────┐ ┌──────────────┐
|
|
│ customers│──Copy──>│ raw_customers│──Notebook>│ customer_360 │
|
|
└──────────┘ └──────────────┘ └──────┬───────┘
|
|
│
|
|
Blob Storage Lakehouse (Bronze) │
|
|
┌──────────┐ ┌──────────────┐ │ Notebook
|
|
│ events │──Copy──>│ raw_events │──Notebook──>─────┘
|
|
└──────────┘ └──────────────┘ │
|
|
▼
|
|
┌──────────────┐
|
|
│ Gold: │
|
|
│ ml_features │
|
|
└──────┬───────┘
|
|
│
|
|
┌──────▼───────┐
|
|
│ ML Experiment│
|
|
│ (MLflow) │
|
|
└──────┬───────┘
|
|
│
|
|
┌──────▼───────┐
|
|
│ Power BI │
|
|
│ Dashboard │
|
|
└──────────────┘
|
|
```
|
|
|
|
### Tilgang til lineage
|
|
|
|
Lineage i Fabric er tilgjengelig fra:
|
|
1. **Workspace toolbar**: Velg "Lineage view"
|
|
2. **Item options menu**: Høyreklikk på element → "View lineage"
|
|
3. **Item details page**: Under menyelementer øverst
|
|
4. **Purview Unified Catalog**: Browse → Microsoft Fabric → Fabric Workspaces
|
|
|
|
### Materialized Lake Views med auto-lineage
|
|
|
|
```sql
|
|
-- Materialized Lake Views genererer automatisk lineage
|
|
CREATE SCHEMA IF NOT EXISTS silver;
|
|
|
|
CREATE MATERIALIZED LAKE VIEW IF NOT EXISTS silver.customer_features AS
|
|
SELECT
|
|
c.customer_id,
|
|
c.name,
|
|
c.region,
|
|
COUNT(t.transaction_id) AS total_transactions,
|
|
SUM(t.amount) AS total_amount,
|
|
AVG(t.amount) AS avg_transaction_amount
|
|
FROM bronze.customers c
|
|
JOIN bronze.transactions t ON c.customer_id = t.customer_id
|
|
GROUP BY c.customer_id, c.name, c.region;
|
|
|
|
-- Lineage er automatisk sporet:
|
|
-- bronze.customers + bronze.transactions → silver.customer_features
|
|
```
|
|
|
|
---
|
|
|
|
## Upstream/Downstream Dependency Mapping
|
|
|
|
### Avhengighetsgraf
|
|
|
|
```python
|
|
# Kartlegg avhengigheter programmatisk
|
|
dependency_graph = {
|
|
"bronze.raw_customers": {
|
|
"sources": ["Azure SQL DB: customers_table"],
|
|
"consumers": ["silver.customer_360", "silver.customer_features"]
|
|
},
|
|
"silver.customer_360": {
|
|
"sources": ["bronze.raw_customers", "bronze.raw_contacts", "bronze.raw_opportunities"],
|
|
"consumers": ["gold.churn_features", "gold.revenue_predict_features"]
|
|
},
|
|
"gold.churn_features": {
|
|
"sources": ["silver.customer_360", "silver.transaction_features"],
|
|
"consumers": ["ML Experiment: churn_model_v3", "Power BI: Churn Dashboard"]
|
|
}
|
|
}
|
|
|
|
def get_upstream_dependencies(table_name, graph, depth=0, max_depth=5):
|
|
"""Rekursivt finn alle oppstrøms avhengigheter."""
|
|
if depth > max_depth or table_name not in graph:
|
|
return []
|
|
|
|
sources = graph[table_name].get("sources", [])
|
|
all_upstream = list(sources)
|
|
|
|
for source in sources:
|
|
all_upstream.extend(
|
|
get_upstream_dependencies(source, graph, depth + 1, max_depth)
|
|
)
|
|
|
|
return all_upstream
|
|
|
|
def get_downstream_impact(table_name, graph, depth=0, max_depth=5):
|
|
"""Finn alle nedstrøms konsumenter (impact-analyse)."""
|
|
if depth > max_depth or table_name not in graph:
|
|
return []
|
|
|
|
consumers = graph[table_name].get("consumers", [])
|
|
all_downstream = list(consumers)
|
|
|
|
for consumer in consumers:
|
|
all_downstream.extend(
|
|
get_downstream_impact(consumer, graph, depth + 1, max_depth)
|
|
)
|
|
|
|
return all_downstream
|
|
|
|
# Eksempel: Hva påvirkes hvis vi endrer bronze.raw_customers?
|
|
impact = get_downstream_impact("bronze.raw_customers", dependency_graph)
|
|
print(f"Påvirkede elementer: {impact}")
|
|
# ['silver.customer_360', 'silver.customer_features',
|
|
# 'gold.churn_features', 'gold.revenue_predict_features',
|
|
# 'ML Experiment: churn_model_v3', 'Power BI: Churn Dashboard']
|
|
```
|
|
|
|
### Data Contract Pattern
|
|
|
|
```python
|
|
# Definer data contracts mellom lag
|
|
data_contract = {
|
|
"table": "silver.customer_360",
|
|
"version": "2.1",
|
|
"owner": "data-engineering@example.no",
|
|
"sla": {
|
|
"freshness": "24 hours",
|
|
"completeness": "> 99.5%",
|
|
"accuracy": "validated against source"
|
|
},
|
|
"schema": {
|
|
"required_columns": ["customer_id", "name", "region", "total_revenue"],
|
|
"column_types": {
|
|
"customer_id": "string",
|
|
"total_revenue": "double",
|
|
"region": "string"
|
|
},
|
|
"partitioned_by": ["region"],
|
|
"row_count_range": [100000, 500000]
|
|
},
|
|
"consumers": [
|
|
{"team": "ml-team", "usage": "churn prediction features"},
|
|
{"team": "bi-team", "usage": "customer dashboard"}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rollback and Recovery Strategies
|
|
|
|
### Delta Lake RESTORE
|
|
|
|
```sql
|
|
-- Gjenopprett tabell til spesifikk versjon
|
|
RESTORE TABLE gold.ml_training_data TO VERSION AS OF 2;
|
|
|
|
-- Gjenopprett til tidspunkt
|
|
RESTORE TABLE gold.ml_training_data TO TIMESTAMP AS OF '2026-02-01T00:00:00Z';
|
|
```
|
|
|
|
```python
|
|
# PySpark RESTORE
|
|
dt = DeltaTable.forPath(spark, "Tables/gold/ml_training_data")
|
|
dt.restoreToVersion(2)
|
|
|
|
# Verifiser
|
|
print(f"Gjenopprettet til versjon 2")
|
|
print(f"Rader: {spark.read.format('delta').table('gold.ml_training_data').count()}")
|
|
```
|
|
|
|
### Recovery-strategier
|
|
|
|
| Scenario | Strategi | Kommando |
|
|
|---|---|---|
|
|
| **Feilaktig DELETE** | Restore til forrige versjon | `RESTORE TABLE ... TO VERSION AS OF n-1` |
|
|
| **Korrupt data lastet** | Restore til pre-load versjon | `RESTORE TABLE ... TO TIMESTAMP AS OF '...'` |
|
|
| **Schema-feil** | Restore + re-apply korrekt schema | Restore + ALTER TABLE |
|
|
| **Hel tabell tapt** | Gjenskape fra kildedata + audit log | Kjør pipeline på nytt |
|
|
| **VACUUM kjørt for tidlig** | Ingen recovery mulig! | Forebygg: minimum 7d retention |
|
|
|
|
### Rollback av ML-eksperiment
|
|
|
|
```python
|
|
import mlflow
|
|
|
|
def rollback_to_experiment(run_id):
|
|
"""Gjenopprett data og modell fra en tidligere MLflow-run."""
|
|
# Hent metadata fra MLflow
|
|
run = mlflow.get_run(run_id)
|
|
data_version = int(run.data.params["data_version"])
|
|
data_table = run.data.params["data_table"]
|
|
|
|
# Gjenopprett treningsdata
|
|
df_original = spark.read.format("delta") \
|
|
.option("versionAsOf", data_version) \
|
|
.table(data_table)
|
|
|
|
print(f"Gjenopprettet data fra versjon {data_version}")
|
|
print(f"Rader: {df_original.count()}")
|
|
|
|
# Last modell
|
|
model = mlflow.sklearn.load_model(f"runs:/{run_id}/model")
|
|
|
|
return df_original, model
|
|
```
|
|
|
|
### Forebyggende tiltak
|
|
|
|
```python
|
|
# 1. Sett minimum VACUUM-retensjon
|
|
spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "true")
|
|
# Standard 7 dager = 168 timer
|
|
|
|
# 2. Aktiver Change Data Feed for sporbar endringshåndtering
|
|
spark.sql("""
|
|
ALTER TABLE gold.ml_training_data
|
|
SET TBLPROPERTIES (delta.enableChangeDataFeed = true)
|
|
""")
|
|
|
|
# 3. Les Change Data Feed
|
|
changes = spark.read.format("delta") \
|
|
.option("readChangeFeed", "true") \
|
|
.option("startingVersion", 3) \
|
|
.option("endingVersion", 4) \
|
|
.table("gold.ml_training_data")
|
|
|
|
# Viser _change_type: insert, update_preimage, update_postimage, delete
|
|
display(changes.select("customer_id", "_change_type", "_commit_version", "_commit_timestamp"))
|
|
```
|
|
|
|
---
|
|
|
|
## Referanser
|
|
|
|
- [Lineage in Fabric](https://learn.microsoft.com/en-us/fabric/governance/lineage) -- Innebygd lineage-visning i Fabric
|
|
- [How to get lineage from Microsoft Fabric items into Microsoft Purview](https://learn.microsoft.com/en-us/purview/data-map-lineage-fabric) -- Purview lineage for Fabric
|
|
- [Data lineage in classic Data Catalog](https://learn.microsoft.com/en-us/purview/data-gov-classic-lineage) -- Lineage-konsepter og granularitet
|
|
- [Delta Lake table format interoperability](https://learn.microsoft.com/en-us/fabric/fundamentals/delta-lake-interoperability) -- Delta Lake-versjonering
|
|
- [What is Delta Lake?](https://learn.microsoft.com/en-us/azure/synapse-analytics/spark/apache-spark-what-is-delta-lake) -- Delta Lake oversikt med time-travel
|
|
- [Get started with materialized lake views](https://learn.microsoft.com/en-us/fabric/data-engineering/materialized-lake-views/get-started-with-materialized-lake-views) -- Auto-lineage via materialized views
|
|
- [Data lineage (Cloud Adoption Framework)](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/govern-lineage) -- Lineage-strategi
|
|
|
|
---
|
|
|
|
## For Cosmo
|
|
|
|
- **Bruk denne referansen** når brukeren trenger reproduserbarhet for ML-modeller, audit trail for AI-beslutninger, eller impact-analyse for dataendringer.
|
|
- For norsk offentlig sektor: **Versjonskontroll er ikke valgfritt** for AI-systemer som påvirker borgere. EU AI Act krever sporbarhet for høyrisiko-systemer, og Utredningsinstruksen krever dokumentasjon av beslutningsgrunnlag.
|
|
- Anbefal å logge **Delta-tabell-versjon** som MLflow-parameter for hvert eksperiment -- dette er den enkleste veien til reproduserbar ML.
|
|
- **Change Data Feed** er kraftig for å forstå eksakt hva som endret seg mellom versjoner -- aktiver dette for alle Gold-tabeller som brukes til ML-trening.
|
|
- **VACUUM-advarsel**: Sørg for at VACUUM-retensjon er lang nok til å dekke alle aktive eksperimenter. 30 dager er et godt utgangspunkt for organisasjoner med ukentlige treningssykluser.
|