mcpField
C
creating-openlineage-extractors

creating-openlineage-extractors

github.com/astronomerデータ・データベース457938 回インストール3 週間前に更新
リポジトリ

creating-openlineage-extractors について

組み込みの対応がない Airflow オペレーターに OpenLineage の収集機能を追加するためのガイドです。オペレーターメソッドとカスタム Extractor の実装、登録、テスト方法を説明します。

主な機能

  1. 012種類のリネージ実装方法の比較
  2. 02入力・出力・実行メタデータの抽出
  3. 03実行前および成功・失敗後の抽出
  4. 04設定ファイルまたは環境変数での登録
  5. 05テストとよくある問題の解説

活用シーン

  1. 01自作 Airflow オペレーターへのリネージ追加
  2. 02変更できないサードパーティ製オペレーターからのリネージ取得
  3. 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 は、次の順序でリネージを確認します。

  1. カスタム抽出器(最優先)
  2. オペレーターの OpenLineage メソッド
  3. フックレベルのリネージ(HookLineageCollector から)
  4. Inlets/Outlets(優先度が最も低い)

カスタム抽出器が存在する場合、組み込みの抽出および inlets/outlets は上書きされます。


関連スキル

  • annotating-task-lineage:inlet/outlet を使ったシンプルなテーブルレベルのリネージュに使用します
  • tracing-upstream-lineage:データの起源を調査します
  • tracing-downstream-lineage:データの依存関係を調査します

コメント