はじめに
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 ステップです。
- アプリから span を送り、
_otel_spansテーブルに格納されることを確認する -
mlflow.log_feedback()でスコアを書き込み、評価データがトレースに紐づくことを確認する - MLflow UI と Delta テーブルが同じデータを見ていることを確認する
- 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_prefix は mlflow.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、そして各テーブルへの MODIFY と SELECT が必要です。ALL_PRIVILEGES だけでは不十分で、MODIFY と SELECT を明示的に付与する必要があります。
セットアップコードを実行すると、カタログエクスプローラーのスキーマに _otel_spans / _otel_logs / _otel_annotations / _otel_metrics の 4 テーブルが作成されます。
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_feedback、get_trace など) |
URI 形式 trace:/catalog.schema.prefix/<uuid>
|
SQL クエリの WHERE trace_id = ...
|
末尾の UUID 部分のみ |
この時点で、対象エクスペリメントのトレース詳細ビューに 3 つの span が階層構造で表示されているはずです。
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 が表示されます。
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() の結果はそのまま棒グラフ等で可視化できます。日別の件数推移をグラフ化したものを記事に添えると、運用ダッシュボードのイメージが伝わりやすくなります。
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 を書かなくても主要な集計をひと目で確認できます。
概要ビューには、指定期間 (画面上部で時間単位と期間を切り替え可能) の以下の指標が自動で集計・可視化されます。
- トレース件数の推移
- レイテンシの 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





