0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MLflowのトレースをUnity Catalogに保存してSQLで分析する ― OpenTelemetry連携ウォークスルー

0
Posted at

はじめに

MLflow 3 のトレースは、デフォルトでは MLflow コントロールプレーンに保存されます。これに対して、トレースデータを OpenTelemetry (OTel) 形式で Unity Catalog の Delta テーブルに保存する UC trace storage という選択肢があります (2026年5月時点でパブリックプレビュー)。

この方式の最大のメリットは、トレースを通常の Delta テーブルとして SQL で直接分析できることです。MLflow UI だけでは難しいレイテンシのパーセンタイル分析や日別件数の推移、エラートレースの抽出などが、見慣れた spark.sql() で実現できます。

本記事は、以下の 2 つの公式ドキュメントを実際にノートブックでウォークスルーした内容をベースに、UC trace storage のセットアップから SQL 分析までを一連の流れとして整理したものです。ノートブックはGitHub リポジトリに置いてあります。

ストーリーは次の 4 ステップです。

  1. アプリから span を送り、_otel_spans テーブルに格納されることを確認する
  2. mlflow.log_feedback() でスコアを書き込み、評価データがトレースに紐づくことを確認する
  3. MLflow UI と Delta テーブルが同じデータを見ていることを確認する
  4. Delta テーブルを SQL で分析する

OpenTelemetryトレースをUnity Catalogに保存するとは

UC trace storage を有効にすると、トレースデータは OTel 準拠の 4 つの Delta テーブルに格納されます。

テーブル 役割
<prefix>_otel_spans アプリが出力した span を格納
<prefix>_otel_logs アセスメント (評価スコア) を OTel ログ形式で格納
<prefix>_otel_annotations MLflow のメタデータ・タグ・評価・実行リンク
<prefix>_otel_metrics カスタム計装したメトリクス (任意)

公式ドキュメントが挙げる主なメリットは以下のとおりです。

  • アクセス制御をエクスペリメントレベルの ACL ではなく、Unity Catalog のスキーマ・テーブル権限で管理できる
  • トレース ID が tr-<UUID> 形式ではなく URI 形式になり、外部システムとの互換性が高まる
  • 大量のトレースデータを Delta テーブルに保存でき、長期保存と分析が可能になる
  • Databricks SQL ウェアハウス経由で SQL によるトレース分析ができる
  • OTel 形式のため、他の OpenTelemetry クライアント・ツールとの互換性が保たれる

前提条件

ドキュメントに記載の前提条件は次のとおりです。

  • Unity Catalog 対応のワークスペース
  • ワークスペース管理者がプレビュー機能を有効化していること (Databricks 上の OpenTelemetry、および 半構造化データの読み取り最適化のためのバリアントシュレッディング)
  • Unity Catalog でカタログとスキーマを作成する権限
  • CAN USE 権限を持つ Databricks SQL ウェアハウス (ウェアハウス ID を控えておく)
  • サポート対象リージョンのワークスペース
  • MLflow Python ライブラリ 3.11 以降

Part 1: セットアップ

UCトレースストレージのテーブル命名ルール

UC trace storage では、テーブル名の先頭に table_prefix が付与されます。

<catalog>.<schema>.<table_prefix>_otel_spans
<catalog>.<schema>.<table_prefix>_otel_logs
<catalog>.<schema>.<table_prefix>_otel_annotations
<catalog>.<schema>.<table_prefix>_otel_metrics

table_prefixmlflow.set_experiment()trace_location で指定します。省略した場合はエクスペリメント ID が自動的に使われます。

指定方法 作成されるテーブル名
明示的に指定: table_prefix="llmops_demo" llmops_demo main.mlflow_traces.llmops_demo_otel_spans
省略 (エクスペリメント ID が自動で使われる) 12345 main.mlflow_traces.12345_otel_spans

テーブル名が予測しやすくなるため、本記事では table_prefix を明示的に指定します。

独立エクスペリメントを使う理由

通常の Databricks ノートブックでは、mlflow.set_experiment() を呼ばなくてもノートブックエクスペリメントが自動的に使われます。

しかし UC trace storage を使う場合は trace_location の指定が必須で、そのためには mlflow.set_experiment() を明示的に呼ぶ必要があります。この API は experiment_name が必須パラメータのため、独立したエクスペリメントとして実験名を指定します。

この方式には利点もあります。複数のノートブックから同じエクスペリメントにトレースを記録でき、エクスペリメントの管理が明確になります。

セットアップコード

まず MLflow をインストールします。

%pip install "mlflow>=3.11"
dbutils.library.restartPython()

続いて、エクスペリメントを Unity Catalog のトレース場所にバインドします。

import os
import mlflow
from mlflow.entities.trace_location import UnityCatalog
from mlflow.entities import AssessmentSource

mlflow.set_tracking_uri("databricks")

# UC trace storage の保存先
CATALOG_NAME = "takaakiyayoi_catalog"
SCHEMA_NAME = "mlflow_llmops"
TABLE_PREFIX_NAME = "llmops_demo"

# UC trace storage のクエリに使用する SQL Warehouse
os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = "bec52b183a4cfe2a"

# UC trace storage を使うには mlflow.set_experiment() の明示呼び出しが必要
# そのため独立エクスペリメントとして実験名を指定する
EXPERIMENT_NAME = "/Users/takaaki.yayoi@databricks.com/llmops_demo"
mlflow.set_experiment(
    experiment_name=EXPERIMENT_NAME,
    trace_location=UnityCatalog(
        catalog_name=CATALOG_NAME,
        schema_name=SCHEMA_NAME,
        table_prefix=TABLE_PREFIX_NAME,
    ),
)

# 後続の SQL クエリで使うテーブル名のプレフィックス
TABLE_PREFIX = f"{CATALOG_NAME}.{SCHEMA_NAME}.{TABLE_PREFIX_NAME}"

print(f"Experiment: {EXPERIMENT_NAME}")
print(f"テーブル例: {TABLE_PREFIX}_otel_spans")

mlflow.set_experiment() は upsert 操作です。実行すると、カタログエクスプローラーのスキーマに 4 つのテーブルが作成されます。なお、エクスペリメントは作成時に UC トレース場所へバインドされ、後から別の場所に再割り当てすることはできません (複数エクスペリメントが同じ場所を共有することは可能です)。

権限についての注意
UC トレーステーブルへの書き込み・読み取りには、カタログへの USE_CATALOG、スキーマへの USE_SCHEMA、そして各テーブルへの MODIFYSELECT が必要です。ALL_PRIVILEGES だけでは不十分で、MODIFYSELECT を明示的に付与する必要があります。

セットアップコードを実行すると、カタログエクスプローラーのスキーマに _otel_spans / _otel_logs / _otel_annotations / _otel_metrics の 4 テーブルが作成されます。

Screenshot 2026-05-20 at 15.19.24.png

Part 2: Spanの送信

アプリが出力すべき OTel シグナルは span です。@mlflow.trace デコレータで span を生成します。今回は RAG エージェントを模した 3 つの関数を用意します。

@mlflow.trace(name="retrieve_context", span_type="RETRIEVER")
def retrieve_context(query: str) -> str:
    """検索ステップをシミュレート"""
    return f"'{query}' に関連するコンテキスト情報"


@mlflow.trace(name="generate_response", span_type="LLM")
def generate_response(query: str, context: str) -> str:
    """LLM 呼び出しをシミュレート"""
    return f"回答: {query} について — {context} に基づく回答です。"


@mlflow.trace(name="agent_pipeline", span_type="AGENT")
def agent_pipeline(query: str) -> str:
    """エージェントのパイプライン全体 (root span)"""
    context = retrieve_context(query)
    response = generate_response(query, context)
    return response


result = agent_pipeline("Databricksのセキュリティ機能について教えてください")
print(result)

生成される span 階層は次のようになります。

agent_pipeline (AGENT)          ← root span
  ├── retrieve_context (RETRIEVER)
  └── generate_response (LLM)

root span さえあれば MLflow エクスペリメントにトレースが表示されます。フレームワークが span のみに対応していても問題ありません。

トレース ID の形式に注意

UC trace storage では、トレース ID が URI 形式で返されます。MLflow API に渡す形式と、SQL クエリで使う形式が異なる点に注意してください。

# UC trace storage の場合、trace ID は URI 形式で返される
# 例: "trace:/catalog.schema.prefix/75edd04e18ea08a7d83ffb9f237c5d7d"
trace_id_full = mlflow.get_last_active_trace_id()

# MLflow API (log_feedback 等) には URI 形式をそのまま渡す
# SQL クエリには末尾の ID 部分のみを使う (テーブル内の trace_id カラムの形式)
trace_id_for_sql = trace_id_full.rsplit("/", 1)[-1]

print(f"Trace ID (API 用): {trace_id_full}")
print(f"Trace ID (SQL 用): {trace_id_for_sql}")
用途 使う形式
MLflow API (log_feedbackget_trace など) URI 形式 trace:/catalog.schema.prefix/<uuid>
SQL クエリの WHERE trace_id = ... 末尾の UUID 部分のみ

この時点で、対象エクスペリメントのトレース詳細ビューに 3 つの span が階層構造で表示されているはずです。

Screenshot 2026-05-20 at 15.20.22.png

Part 3: スコア (Assessment) の書き込み

評価スコアは assessment としてトレースに紐付けます。mlflow.log_feedback() を使い、人間のフィードバック・自動評価スコア・正解との比較の 3 種類を書き込みます。

# 人間のフィードバック
mlflow.log_feedback(
    trace_id=trace_id_full,
    name="user_satisfaction",
    value=4.0,
    source=AssessmentSource(source_type="HUMAN", source_id="demo_user"),
    rationale="回答は正確だが、もう少し具体例が欲しい",
)

# 自動評価スコア
mlflow.log_feedback(
    trace_id=trace_id_full,
    name="relevance_score",
    value=0.85,
    source=AssessmentSource(source_type="CODE", source_id="relevance_scorer"),
    rationale="コンテキストと回答の関連性が高い",
)

# 正解との比較
mlflow.log_feedback(
    trace_id=trace_id_full,
    name="correctness",
    value=True,
    source=AssessmentSource(source_type="HUMAN", source_id="demo_user"),
    rationale="期待される回答と一致",
)

print("スコアを3件書き込みました")

書き込んだスコアは次の場所に反映されます。

反映先 表示内容
MLflow UI — トレースリスト 各 assessment 名が独立カラムとして表示
MLflow UI — トレース詳細 Assessments パネルに値と rationale が表示
_otel_logs テーブル OTel ログ形式の構造化イベントとして格納

ここで重要なのは、アプリが出力するのは span のみである点です。アセスメントは mlflow.log_feedback() を呼んだときに、プラットフォーム側が OTel ログ形式に変換して格納します。

書き込み後、MLflow UI のトレースリストには user_satisfaction / relevance_score / correctness が独立カラムとして表示され、トレース詳細の Assessments パネルには各スコアの値と rationale が表示されます。

Screenshot 2026-05-20 at 15.21.50.png

Screenshot 2026-05-20 at 15.21.16.png

Part 4: Delta Tableの中身を確認

ここからが Delta テーブルと MLflow UI の関係の核心です。UC trace storage では、MLflow UI が読み取るデータはすべて Delta テーブルに格納されています

ドキュメントでは、基盤テーブル (_otel_spans 等) のスキーマは変更される可能性があるため、自動生成されるビュー経由でクエリすることが推奨されています。

クエリ対象 説明
<prefix>_trace_unified span + assessment + メタデータを結合したビュー (推奨)。1 行 = 1 トレース
<prefix>_trace_metadata トレース単位のメタデータ・タグ・assessment。MLflow データの取得は統合ビューより高速
<prefix>_otel_spans 基盤テーブル。スキーマ変更の可能性があり直接利用は非推奨

4-1. trace_unified ビューでトレース全体を確認

_trace_unified はトレース単位のビューです。span データは spans カラム (LIST<STRUCT>) にネストされています。

# トレース全体の情報を確認
df_trace = spark.sql(f"""
    SELECT
        trace_id,
        request_time,
        state,
        execution_duration_ms,
        request,
        response
    FROM {TABLE_PREFIX}_trace_unified
    WHERE trace_id = '{trace_id_for_sql}'
""")

display(df_trace)

spans カラムを explode で展開すると、個別の span を確認できます。

# spans カラムを展開して個別の span を確認
df_spans = spark.sql(f"""
    SELECT
        s.span_id,
        s.parent_span_id,
        s.name,
        s.start_time_unix_nano,
        s.end_time_unix_nano,
        s.status.code AS status_code,
        s.attributes
    FROM {TABLE_PREFIX}_trace_unified,
    LATERAL VIEW explode(spans) AS s
    WHERE trace_id = '{trace_id_for_sql}'
    ORDER BY s.start_time_unix_nano
""")

display(df_spans)

3 つの span (agent_pipeline、retrieve_context、generate_response) が parent_span_id で階層構造を持っていることが確認できます。

4-2. trace_metadata ビューでアセスメントを確認

_trace_metadata ビューでアセスメントを確認します。

df_metadata = spark.sql(f"""
    SELECT
        trace_id,
        assessments,
        tags,
        trace_metadata
    FROM {TABLE_PREFIX}_trace_metadata
    WHERE trace_id = '{trace_id_for_sql}'
""")

display(df_metadata)

assessments カラム (LIST<STRUCT>) に、mlflow.log_feedback() で書き込んだ 3 件のアセスメントが含まれています。これが MLflow UI のトレースリストで Assessment カラムとして表示される実体です。

4-3. 基盤テーブルの行数を確認

基盤テーブルにどのデータが格納されているかを行数で確認します (スキーマは変更される可能性があるため、あくまで参考程度に)。

# 基盤テーブルの行数を確認
for table in ["_otel_spans", "_otel_logs", "_otel_annotations", "_otel_metrics"]:
    count = spark.sql(f"SELECT COUNT(*) AS cnt FROM {TABLE_PREFIX}{table}").first()["cnt"]
    print(f"{TABLE_PREFIX}{table}: {count}")
テーブル 確認ポイント
_otel_spans span が格納されている
_otel_logs アセスメントが格納されている
_otel_metrics カスタム計装をしていないため空 (0 件)。MLflow UI にも非表示
_otel_annotations MLflow 固有のメタデータ・タグ・評価・実行リンク

MLflow UI はこれらの Delta テーブルを直接読み取っています。SQL でテーブルを変更すれば UI にも反映されます (スキーマの整合性は維持する必要があります)。

Part 5: SQLによるトレース分析

ここからが UC trace storage の本領です。MLflow UI では難しい分析が SQL で可能になります。

5-1. 直近のトレース一覧

df = spark.sql(f"""
    SELECT
        trace_id,
        request_time,
        state,
        execution_duration_ms,
        request,
        response
    FROM {TABLE_PREFIX}_trace_unified
    ORDER BY request_time DESC
    LIMIT 20
""")

display(df)

5-2. アセスメント一覧

アセスメントの確認には、軽量な _trace_metadata ビューを使います。

df = spark.sql(f"""
    SELECT
        trace_id,
        assessments
    FROM {TABLE_PREFIX}_trace_metadata
    WHERE assessments IS NOT NULL
    LIMIT 50
""")

display(df)

5-3. レイテンシ分析

execution_duration_ms を使い、平均・パーセンタイル・最大値を集計します。MLflow UI では難しい p50 / p95 の算出も SQL なら一発です。

df = spark.sql(f"""
    SELECT
        COUNT(*) AS trace_count,
        CAST(AVG(execution_duration_ms) AS DECIMAL(10,1)) AS avg_ms,
        CAST(PERCENTILE(execution_duration_ms, 0.5) AS DECIMAL(10,1)) AS p50_ms,
        CAST(PERCENTILE(execution_duration_ms, 0.95) AS DECIMAL(10,1)) AS p95_ms,
        CAST(MAX(execution_duration_ms) AS DECIMAL(10,1)) AS max_ms
    FROM {TABLE_PREFIX}_trace_unified
""")

display(df)

5-4. 日別トレース件数の推移

df = spark.sql(f"""
    SELECT
        DATE(request_time) AS date,
        COUNT(*) AS trace_count
    FROM {TABLE_PREFIX}_trace_unified
    GROUP BY DATE(request_time)
    ORDER BY date DESC
    LIMIT 30
""")

display(df)

display() の結果はそのまま棒グラフ等で可視化できます。日別の件数推移をグラフ化したものを記事に添えると、運用ダッシュボードのイメージが伝わりやすくなります。

Screenshot 2026-05-20 at 15.22.33.png

5-5. エラーが発生したトレースの特定

まずエラーを発生させるトレースを作成し、その後 state = 'ERROR' で検索します。

# エラーを発生させるトレースを作成
@mlflow.trace(name="failing_pipeline", span_type="AGENT")
def failing_pipeline(query: str) -> str:
    raise ValueError(f"処理に失敗しました: {query}")

try:
    failing_pipeline("エラーテスト")
except ValueError:
    pass

import time
print("エラートレースを作成しました。テーブルへの反映を待ちます...")
time.sleep(10)

# エラーが発生したトレースを検索
df = spark.sql(f"""
    SELECT
        trace_id,
        request_time,
        state,
        execution_duration_ms,
        request
    FROM {TABLE_PREFIX}_trace_unified
    WHERE state = 'ERROR'
    ORDER BY request_time DESC
    LIMIT 20
""")

display(df)

クエリパフォーマンスのヒント
トレースデータ量が大きい場合、ビューへのクエリは遅くなることがあります。ドキュメントでは、ビューの上にマテリアライズドビューを作成して増分更新する方法が推奨されています。最新データで最高のパフォーマンスを得たい場合は、SQL ではなく MLflow Python SDK (mlflow.search_traces) でクエリする選択肢もあります。

概要ビューで集計を確認する

ここまでは SQL でトレースを分析してきましたが、MLflow UI のエクスペリメント画面には 概要 (Overview) ビューがあり、SQL を書かなくても主要な集計をひと目で確認できます。

Screenshot 2026-05-20 at 15.23.40.png

概要ビューには、指定期間 (画面上部で時間単位と期間を切り替え可能) の以下の指標が自動で集計・可視化されます。

  • トレース件数の推移
  • レイテンシの p50 / p90 / p99
  • エラー件数とエラー率
  • トークン使用量、トレースごとのトークン数

本ノートブックを実行した直後であれば、Part 2 〜 5 で送信した通常トレースと、Part 5-5 で意図的に発生させたエラートレースが、それぞれ件数とエラー率に反映されているはずです。UC trace storage では、これらの集計も Delta テーブルを読み取って描画されています。

概要ビューと SQL 分析は補完関係にあります。日常的な監視は概要ビューでひと目で把握し、パーセンタイルの厳密な集計や任意条件での絞り込みといった踏み込んだ分析は SQL で行う、という使い分けが現実的です。

データフローとまとめ

最後に、各 OTel シグナルがどのテーブルに入り、MLflow UI のどこに表示されるかを整理します。

発生源 格納先テーブル MLflow UI での表示
アプリの @mlflow.trace _otel_spans トレース詳細ビュー (span 階層)
mlflow.log_feedback() _otel_logs トレースリストの Assessment カラム / トレース詳細の Assessments パネル
カスタム計装 (任意) _otel_metrics UI には非表示。SQL で直接参照する必要あり

押さえておきたい要点は次のとおりです。

  • アプリが出力するのは span のみ。アセスメントは mlflow.log_feedback() 呼び出し時にプラットフォームが OTel 形式へ変換して格納する
  • _otel_metrics はカスタム計装をしなければ空。MLflow UI にも表示されない
  • MLflow UI は Delta テーブルを直接読み取っている。SQL でテーブルを変更すれば UI にも反映される
  • SQL クエリは基盤テーブルではなく _trace_unified / _trace_metadata ビューに対して実行する。基盤テーブルのスキーマは変更される可能性がある
  • トレース ID は MLflow API では URI 形式、SQL では末尾の UUID 部分を使う

UC trace storage を使うことで、トレースが「ブラックボックスな観測データ」から「SQL で自由に分析できる Delta テーブル」へと変わります。レイテンシのパーセンタイル監視、エラー率の集計、評価スコアの傾向分析など、LLMOps の運用フェーズで必要になる分析を、Databricks の既存のデータ基盤の上でそのまま実現できる点が大きな魅力です。

なお本機能は 2026年5月時点でパブリックプレビューです。トレース取り込みのレート制限や、個別トレースの削除が SQL 経由のみといった制限事項があるため、利用前に公式ドキュメントの制限事項を確認してください。

検証ノートブック

本記事のセットアップから SQL 分析までを通しで実行できるノートブックを GitHub に置いています。span の送信、スコアの書き込み、_trace_unified / _trace_metadata ビューの確認、各種 SQL 分析が 1 つのノートブックで完結します。

mlflow-llmops-articles/otel-uc-trace-storage

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?