creating-openlineage-extractors について
組み込みの対応がない Airflow オペレーターに OpenLineage の収集機能を追加するためのガイドです。オペレーターメソッドとカスタム Extractor の実装、登録、テスト方法を説明します。
主な機能
- 012種類のリネージ実装方法の比較
- 02入力・出力・実行メタデータの抽出
- 03実行前および成功・失敗後の抽出
- 04設定ファイルまたは環境変数での登録
- 05テストとよくある問題の解説
活用シーン
- 01自作 Airflow オペレーターへのリネージ追加
- 02変更できないサードパーティ製オペレーターからのリネージ取得
- 03実行時に決まる入出力の記録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ astronomer/ agents --skill creating-openlineage-extractorsインストール後、Claude Code で/creating-openlineage-extractorsを入力して読み込まれたか確認します。
詳細
OpenLineage 抽出器の作成
このスキルでは、組み込みサポートのない Airflow オペレーターからリネージを取得するためのカスタム OpenLineage 抽出器の作成方法を説明します。
参考: 最新のパターンとサポート対象のオペレーター/フックの一覧については、OpenLineage プロバイダー開発者ガイドを参照してください。
各アプローチの使い分け
| シナリオ | アプローチ |
|---|---|
| 自分が所有・保守するオペレーター | OpenLineage メソッド(推奨、最も簡単) |
| 変更できないサードパーティ製オペレーター | カスタム抽出器 |
| カラムレベルのリネージが必要 | OpenLineage メソッドまたはカスタム抽出器 |
| 複雑な抽出ロジック | OpenLineage メソッドまたはカスタム抽出器 |
| シンプルなテーブルレベルのリネージ | Inlets/Outlets(最も簡単ですが、優先度は最も低い) |
重要: 可能な場合は、常にカスタム抽出器より OpenLineage メソッドを優先してください。抽出器は記述が難しく、変更後にオペレーターの動作との乖離が生じやすく、デバッグも困難です。
Astro の場合
Astro には OpenLineage の統合機能が組み込まれているため、追加の転送設定は不要です。リネージイベントは自動的に収集され、Astro UI の Lineage タブに表示されます。Astro プロジェクトにデプロイしたカスタム抽出器は自動的に認識されるため、airflow.cfg または環境変数で登録してデプロイするだけで済みます。
2 つのアプローチ
1. OpenLineage メソッド(推奨)
カスタムオペレーターにメソッドを直接追加できる場合に使用します。自分が所有するオペレーターには、第一の選択肢となります。
2. カスタム抽出器
変更できないサードパーティ製またはプロバイダー製のオペレーターからリネージを取得する必要がある場合に使用します。
アプローチ 1:OpenLineage メソッド(推奨)
オペレーターを所有している場合は、OpenLineage メソッドを直接追加します。
from airflow.models import BaseOperator
class MyCustomOperator(BaseOperator):
"""Custom operator with built-in OpenLineage support."""
def __init__(self, source_table: str, target_table: str, **kwargs):
super().__init__(**kwargs)
self.source_table = source_table
self.target_table = target_table
self._rows_processed = 0 # Set during execution
def execute(self, context):
# Do the actual work
self._rows_processed = self._process_data()
return self._rows_processed
def get_openlineage_facets_on_start(self):
"""Called when task starts. Return known inputs/outputs."""
# Import locally to avoid circular imports
from openlineage.client.event_v2 import Dataset
from airflow.providers.openlineage.extractors import OperatorLineage
return OperatorLineage(
inputs=[Dataset(namespace="postgres://db", name=self.source_table)],
outputs=[Dataset(namespace="postgres://db", name=self.target_table)],
)
def get_openlineage_facets_on_complete(self, task_instance):
"""Called after success. Add runtime metadata."""
from openlineage.client.event_v2 import Dataset
from openlineage.client.facet_v2 import output_statistics_output_dataset
from airflow.providers.openlineage.extractors import OperatorLineage
return OperatorLineage(
inputs=[Dataset(namespace="postgres://db", name=self.source_table)],
outputs=[
Dataset(
namespace="postgres://db",
name=self.target_table,
facets={
"outputStatistics": output_statistics_output_dataset.OutputStatisticsOutputDatasetFacet(
rowCount=self._rows_processed
)
},
)
],
)
def get_openlineage_facets_on_failure(self, task_instance):
"""Called after failure. Optional - for partial lineage."""
return None
OpenLineage メソッドのリファレンス
| メソッド | 呼び出されるタイミング | 必須 |
|---|---|---|
get_openlineage_facets_on_start() | タスクが RUNNING になる | いいえ |
get_openlineage_facets_on_complete(ti) | タスクが成功する | いいえ |
get_openlineage_facets_on_failure(ti) | タスクが失敗する | いいえ |
必要なメソッドだけを実装してください。未実装のメソッドは、フックレベルのリネージまたは inlets/outlets にフォールスルーします。
アプローチ 2:カスタム抽出器
オペレーターを変更できない場合(例:サードパーティ製またはプロバイダー製のオペレーター)に限り、このアプローチを使用します。
基本構造
from airflow.providers.openlineage.extractors.base import BaseExtractor, OperatorLineage
from openlineage.client.event_v2 import Dataset
class MyOperatorExtractor(BaseExtractor):
"""Extract lineage from MyCustomOperator."""
@classmethod
def get_operator_classnames(cls) -> list[str]:
"""Return operator class names this extractor handles."""
return ["MyCustomOperator"]
def _execute_extraction(self) -> OperatorLineage | None:
"""Called BEFORE operator executes. Use for known inputs/outputs."""
# Access operator properties via self.operator
source_table = self.operator.source_table
target_table = self.operator.target_table
return OperatorLineage(
inputs=[
Dataset(
namespace="postgres://mydb:5432",
name=f"public.{source_table}",
)
],
outputs=[
Dataset(
namespace="postgres://mydb:5432",
name=f"public.{target_table}",
)
],
)
def extract_on_complete(self, task_instance) -> OperatorLineage | None:
"""Called AFTER operator executes. Use for runtime-determined lineage."""
# Access properties set during execution
# Useful for operators that determine outputs at runtime
return None
OperatorLineage の構造
from airflow.providers.openlineage.extractors.base import OperatorLineage
from openlineage.client.event_v2 import Dataset
from openlineage.client.facet_v2 import sql_job
lineage = OperatorLineage(
inputs=[Dataset(namespace="...", name="...")], # Input datasets
outputs=[Dataset(namespace="...", name="...")], # Output datasets
run_facets={"sql": sql_job.SQLJobFacet(query="SELECT...")}, # Run metadata
job_facets={}, # Job metadata
)
抽出メソッド
| メソッド | 呼び出されるタイミング | 用途 |
|---|---|---|
_execute_extraction() | オペレーターの実行前 | 静的/既知のリネージ |
extract_on_complete(task_instance) | 成功後 | 実行時に決定されるリネージ |
extract_on_failure(task_instance) | 失敗後 | エラー発生時の部分的なリネージ |
抽出器の登録
オプション 1:設定ファイル(airflow.cfg)
[openlineage]
extractors = mypackage.extractors.MyOperatorExtractor;mypackage.extractors.AnotherExtractor
オプション 2:環境変数
AIRFLOW__OPENLINEAGE__EXTRACTORS='mypackage.extractors.MyOperatorExtractor;mypackage.extractors.AnotherExtractor'
重要: パスは Airflow ワーカーからインポート可能である必要があります。抽出器は DAG フォルダーに配置するか、パッケージとしてインストールしてください。
よくあるパターン
SQL オペレーター抽出器
from airflow.providers.openlineage.extractors.base import BaseExtractor, OperatorLineage
from openlineage.client.event_v2 import Dataset
from openlineage.client.facet_v2 import sql_job
class MySqlOperatorExtractor(BaseExtractor):
@classmethod
def get_operator_classnames(cls) -> list[str]:
return ["MySqlOperator"]
def _execute_extraction(self) -> OperatorLineage | None:
sql = self.operator.sql
conn_id = self.operator.conn_id
# Parse SQL to find tables (simplified example)
# In practice, use a SQL parser like sqlglot
inputs, outputs = self._parse_sql(sql)
namespace = f"postgres://{conn_id}"
return OperatorLineage(
inputs=[Dataset(namespace=namespace, name=t) for t in inputs],
outputs=[Dataset(namespace=namespace, name=t) for t in outputs],
job_facets={
"sql": sql_job.SQLJobFacet(query=sql)
},
)
def _parse_sql(self, sql: str) -> tuple[list[str], list[str]]:
"""Parse SQL to extract table names. Use sqlglot for real parsing."""
# Simplified example - use proper SQL parser in production
inputs = []
outputs = []
# ... parsing logic ...
return inputs, outputs
ファイル転送抽出器
from airflow.providers.openlineage.extractors.base import BaseExtractor, OperatorLineage
from openlineage.client.event_v2 import Dataset
class S3ToSnowflakeExtractor(BaseExtractor):
@classmethod
def get_operator_classnames(cls) -> list[str]:
return ["S3ToSnowflakeOperator"]
def _execute_extraction(self) -> OperatorLineage | None:
s3_bucket = self.operator.s3_bucket
s3_key = self.operator.s3_key
table = self.operator.table
schema = self.operator.schema
return OperatorLineage(
inputs=[
Dataset(
namespace=f"s3://{s3_bucket}",
name=s3_key,
)
],
outputs=[
Dataset(
namespace="snowflake://myaccount.snowflakecomputing.com",
name=f"{schema}.{table}",
)
],
)
実行からの動的なリネージ
from openlineage.client.event_v2 import Dataset
class DynamicOutputExtractor(BaseExtractor):
@classmethod
def get_operator_classnames(cls) -> list[str]:
return ["DynamicOutputOperator"]
def _execute_extraction(self) -> OperatorLineage | None:
# Only inputs known before execution
return OperatorLineage(
inputs=[Dataset(namespace="...", name=self.operator.source)],
)
def extract_on_complete(self, task_instance) -> OperatorLineage | None:
# Outputs determined during execution
# Access via operator properties set in execute()
outputs = self.operator.created_tables # Set during execute()
return OperatorLineage(
inputs=[Dataset(namespace="...", name=self.operator.source)],
outputs=[Dataset(namespace="...", name=t) for t in outputs],
)
よくある落とし穴
1. 循環インポート
問題: Airflow モジュールをトップレベルでインポートすると、循環インポートが発生します。
# ❌ BAD - can cause circular import issues
from airflow.models import TaskInstance
from openlineage.client.event_v2 import Dataset
class MyExtractor(BaseExtractor):
...
# ✅ GOOD - import inside methods
class MyExtractor(BaseExtractor):
def _execute_extraction(self):
from openlineage.client.event_v2 import Dataset
# ...
2. インポートパスの誤り
問題: 抽出器のパスが実際のモジュールの場所と一致していません。
# ❌ Wrong - path doesn't exist
AIRFLOW__OPENLINEAGE__EXTRACTORS='extractors.MyExtractor'
# ✅ Correct - full importable path
AIRFLOW__OPENLINEAGE__EXTRACTORS='dags.extractors.my_extractor.MyExtractor'
3. None の未処理
問題: オペレーターのプロパティが None の場合、抽出に失敗します。
# ✅ Handle optional properties
def _execute_extraction(self) -> OperatorLineage | None:
if not self.operator.source_table:
return None # Skip extraction
return OperatorLineage(...)
抽出器のテスト
単体テスト
import pytest
from unittest.mock import MagicMock
from mypackage.extractors import MyOperatorExtractor
def test_extractor():
# Mock the operator
operator = MagicMock()
operator.source_table = "input_table"
operator.target_table = "output_table"
# Create extractor
extractor = MyOperatorExtractor(operator)
# Test extraction
lineage = extractor._execute_extraction()
assert len(lineage.inputs) == 1
assert lineage.inputs[0].name == "input_table"
assert len(lineage.outputs) == 1
assert lineage.outputs[0].name == "output_table"
優先順位のルール
OpenLineage は、次の順序でリネージを確認します。
- カスタム抽出器(最優先)
- オペレーターの OpenLineage メソッド
- フックレベルのリネージ(
HookLineageCollectorから) - Inlets/Outlets(優先度が最も低い)
カスタム抽出器が存在する場合、組み込みの抽出および inlets/outlets は上書きされます。
関連スキル
- annotating-task-lineage:inlet/outlet を使ったシンプルなテーブルレベルのリネージュに使用します
- tracing-upstream-lineage:データの起源を調査します
- tracing-downstream-lineage:データの依存関係を調査します