TL;DR
- Databricks で OpenTelemetry(OTLP)のトレースを Unity Catalog(UC)に書き込む方法は2つあり、テーブルを指定する HTTP ヘッダー名が異なります(
X-Databricks-UC-Table-Nameとx-databricks-zerobus-table-name)。これは新旧や表記ゆれではなく、宛先エンドポイントが別物であることが理由です。 - どちらの経路も、最終的に UC の Delta テーブルにトレースを書き込みます。実際に両方試したところ、いずれも同じスキーマの
*_otel_spansテーブルに着地しました。 - MLflow の Tracing UI に表示されるかどうかは、エンドポイントの別では決まりません。「Experiment に紐付いたトレース保存場所」+「root span を持つこと」を満たせば、どちらの経路でも表示されます。実際に、MLflow パッケージを使わず生の OpenTelemetry SDK から送ったトレースでも、テーブルを Experiment に紐付けたら MLflow がトレースとして認識しました。
- 実運用上の違いは主に (1) スループット上限、(2) 対象シグナル、(3) テーブルの自動生成 or 事前作成の3点です。
背景 ―― ヘッダーが2種類あって混乱する
Databricks のドキュメントを読むと、OTLP のデータを UC に送る例が2か所に出てきて、テーブルを指定するヘッダー名が違います。
X-Databricks-UC-Table-Namex-databricks-zerobus-table-name
大文字・小文字は RFC 7230 により HTTP ヘッダー名では区別されないので、X-... と x-... の差自体は意味を持ちません。実際に違うのは uc-table-name と zerobus-table-name という名前そのもので、これは送信先エンドポイントが別であることを表しています。
本記事では、この2つのエンドポイントの違い・実際の挙動・使い分けを、手元で動作確認した結果を交えて整理します。
2つのエンドポイント
| 観点 | ① MLflow マネージド OTLP コレクター | ② ネイティブ Zerobus 直接 |
|---|---|---|
| 位置づけ | OTel クライアントからトレースを手早く UC に取り込む簡便な入口 | OTLP テレメトリを高スループットで UC に取り込むネイティブ OTLP エンドポイント |
| エンドポイント | https://<workspace-host>/api/2.0/otel/v1/traces |
https://<workspace-id>.zerobus.<region>.cloud.databricks.com:443/v1/traces |
| テーブル指定ヘッダー | X-Databricks-UC-Table-Name |
x-databricks-zerobus-table-name |
| 対象シグナル | traces | traces / logs / metrics |
| テーブル | 送信時に自動生成 | 事前に CREATE TABLE が必要 |
| 認証 | ワークスペースのトークン |
client_credentials で発行した専用トークン(後述) |
両者は別エンドポイントであり、片方のヘッダーをもう片方のエンドポイントに付けても機能しません。
経路のイメージ
MLflow の UI に表示する条件(エンドポイントに依存しない)
トレースを MLflow の Tracing UI で閲覧できるようにするには、次の2点を満たす必要があります。
- 書き込み先の UC テーブルが、**MLflow Experiment に紐付いた「トレース保存場所(trace location)」**であること
- そのトレースが root span を持つこと
この条件はどちらのエンドポイント経由でも共通で、送信エンドポイントそのものが表示可否を決めるわけではありません。
実際に試してみる
経路① ―― MLflow マネージド OTLP コレクター
MLflow 3.14 以降では、Experiment の「トレース保存場所」を UC テーブルに指定するだけで、@mlflow.trace で記録したトレースがコレクター経由で UC に書き込まれます。
# pip install "mlflow[databricks]>=3.14.0"
import os
import mlflow
from mlflow.entities.trace_location import UnityCatalog
mlflow.set_tracking_uri("databricks")
os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = "<sql_warehouse_id>"
mlflow.set_experiment(
experiment_name="/Users/<you>/otlp_demo",
trace_location=UnityCatalog(
catalog_name="<catalog>",
schema_name="<schema>",
table_prefix="collector", # → collector_otel_spans 等が自動生成される
),
)
@mlflow.trace(span_type="LLM")
def call_model(prompt: str) -> str:
tokens = tokenize(prompt)
return f"echo({len(tokens)} tokens): {prompt}"
@mlflow.trace(span_type="TOOL")
def tokenize(text: str):
return text.split()
call_model("hello managed collector")
確認できたこと
-
<prefix>_otel_spans/_otel_logs/_otel_metrics/_otel_annotationsの4テーブルが自動生成され、加えてトレース用のビューが作られました。 - スパンが UC の Delta テーブルに書き込まれ、各トレースは root span を持っていました。
- テーブルには
mlflow.traceInputs/mlflow.traceOutputs/mlflow.traceNameなどの MLflow 固有メタデータが付与され、MLflow のトレースとして認識されました(Tracing UI で閲覧可)。
経路② ―― ネイティブ Zerobus 直接
こちらは「素の OpenTelemetry」からの取り込みです。手順が①より多く、(a) テーブルの事前作成、(b) 専用トークンの発行、(c) OTLP 送信の3ステップになります。
(a) テーブルを事前作成する
Zerobus 直接経路では、送信先テーブルを OTLP 用スキーマであらかじめ作成しておく必要があります(自動生成されません)。spans テーブルの例(全カラムは公式ドキュメント参照):
CREATE TABLE <catalog>.<schema>.<prefix>_otel_spans (
record_id STRING, time TIMESTAMP, date DATE, service_name STRING,
trace_id STRING, span_id STRING, trace_state STRING, parent_span_id STRING,
flags INT, name STRING, kind STRING,
start_time_unix_nano BIGINT, end_time_unix_nano BIGINT,
attributes VARIANT, dropped_attributes_count INT,
events ARRAY<STRUCT<time_unix_nano: BIGINT, name: STRING, attributes: VARIANT, dropped_attributes_count: INT>>,
dropped_events_count INT,
links ARRAY<STRUCT<trace_id: STRING, span_id: STRING, trace_state: STRING, attributes: VARIANT, dropped_attributes_count: INT, flags: INT>>,
dropped_links_count INT,
status STRUCT<message: STRING, code: STRING>,
resource STRUCT<attributes: VARIANT, dropped_attributes_count: INT>,
resource_schema_url STRING,
instrumentation_scope STRUCT<name: STRING, version: STRING, attributes: VARIANT, dropped_attributes_count: INT>,
span_schema_url STRING
) USING DELTA
CLUSTER BY (time, service_name, trace_id)
TBLPROPERTIES ('otel.schemaVersion' = 'v2', 'delta.checkpointPolicy' = 'classic');
(b) 専用トークンを発行する
Zerobus 直接エンドポイント向けのトークンは、client_credentials グラントで発行します。ポイントは resource に zerobusDirectWriteApi を指定し、書き込み対象テーブルの権限を authorization_details に埋め込む点です(トークンは1時間で失効)。サービスプリンシパルにはあらかじめ対象テーブルへの SELECT, MODIFY(+ USE CATALOG / USE SCHEMA)を付与しておきます。
authorization_details='[
{"type":"unity_catalog_privileges","privileges":["USE CATALOG"],"object_type":"CATALOG","object_full_path":"<catalog>"},
{"type":"unity_catalog_privileges","privileges":["USE SCHEMA"],"object_type":"SCHEMA","object_full_path":"<catalog>.<schema>"},
{"type":"unity_catalog_privileges","privileges":["SELECT","MODIFY"],"object_type":"TABLE","object_full_path":"<catalog>.<schema>.<prefix>_otel_spans"}
]'
curl -X POST -u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=all-apis" \
-d "resource=api://databricks/workspaces/<workspace-id>/zerobusDirectWriteApi" \
--data-urlencode "authorization_details=$authorization_details" \
"https://<workspace-host>/oidc/v1/token"
(c) OTLP で送信する
あとは標準の OpenTelemetry SDK(HTTP/protobuf)で送るだけです。MLflow パッケージは不要です。
# pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
TABLE = "<catalog>.<schema>.<prefix>_otel_spans"
endpoint = "https://<workspace-id>.zerobus.<region>.cloud.databricks.com:443/v1/traces"
exporter = OTLPSpanExporter(
endpoint=endpoint,
headers={
"authorization": f"Bearer {token}", # (b) で発行したトークン
"x-databricks-zerobus-table-name": TABLE, # ← Zerobus 側のヘッダー
},
)
provider = TracerProvider(resource=Resource.create({"service.name": "otlp-demo"}))
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("demo")
with tracer.start_as_current_span("call_model") as root: # root span
root.set_attribute("prompt", "hello zerobus")
with tracer.start_as_current_span("tokenize"):
pass
provider.force_flush()
確認できたこと
- 標準の OTLP HTTP/protobuf + ヘッダーだけで、Zerobus 直接エンドポイントがリクエストを受理しました。
- 事前作成しておいた
<prefix>_otel_spansに、送ったスパンがtrace_id/parent_span_idの親子関係を保ったまま書き込まれ、各トレースは root span を持っていました。 - その後、このテーブルを MLflow Experiment のトレース保存場所に紐付けたところ、
mlflow.search_tracesがトレースを返しました。つまり、MLflow を一切使わず生の OpenTelemetry から送ったトレースでも、条件(Experiment 紐付き+root span)を満たせば MLflow のトレースとして認識され、UI で閲覧できることが確認できました。
スループットと対象シグナルの違い(ドキュメントベース)
大量のトレースを扱う場合に効いてくるのがレート上限です。以下は公式ドキュメントの記載です(手元では上限までの負荷試験は行っていません)。
| 観点 | ① MLflow コレクター | ② Zerobus 直接 |
|---|---|---|
| レート上限 | 200 traces/秒/ワークスペース(プレビュー段階のソフト上限)、100 MB/秒/テーブル | ストリームあたり 100 MB/秒・100,000 records/秒、テーブルあたり 10 GB/秒、REST 10,000 req/秒(いずれも引き上げ可) |
| 対象シグナル | traces | traces / logs / metrics |
- コレクターの 200 traces/秒/ワークスペースは、プレビュー段階の暫定的なソフト上限で、取り込み基盤そのものの限界ではなく、今後の緩和が見込まれます。現時点でも申請でケースバイケースに引き上げ可能とされています。
- Zerobus 直接は、同じ取り込み基盤へ直接送るため、桁違いに高いスループットを想定した経路です(単位が traces / records / requests で異なる点には注意 ―― 1トレース=複数スパン=複数レコード)。
使い分けの指針
-
OTel クライアントからトレースを手早く取り込みたい / 想定量がコレクターの上限内 → ① MLflow マネージド OTLP コレクター。ヘッダーは
X-Databricks-UC-Table-Name。テーブルは自動生成。対象は traces。 -
高スループットが必要 / logs・metrics も取り込みたい / OpenTelemetry 標準の構成で組みたい → ② ネイティブ Zerobus 直接。ヘッダーは
x-databricks-zerobus-table-name。テーブルは事前作成、トークンはzerobusDirectWriteApi向けに発行。 - いずれの場合も、MLflow の UI で閲覧するには 「Experiment に紐付いたトレース保存場所」+「root span」 が前提です。この条件はエンドポイントに依存しません。
まとめ
- 2つのヘッダー(
X-Databricks-UC-Table-Name/x-databricks-zerobus-table-name)は、別々のエンドポイントを指しています。表記ゆれでも新旧でもありません。 - どちらの経路も UC の Delta テーブルに書き込まれ、条件を満たせば MLflow Tracing UI で閲覧できます ―― これは実際に両経路で確認しました。
- 選択の軸は スループット・対象シグナル・テーブルの自動生成/事前作成。手早さなら①、大量取り込み・logs/metrics・OpenTelemetry 標準構成なら②、と覚えておくと迷いません。
参考リンク
- OpenTelemetry(OTLP)クライアントを設定してデータをUnity Catalogに送信する(Zerobus 直接)
- OpenTelemetryトレースをUnity Catalogに保存する(MLflow コレクター)
- Zerobus Ingest コネクタのクォータ
