1. はじめに
Langfuse のセルフホスト版で v4 が公開されました(v4.0.0 は 2026-07-29)1。リリースノートで目立つのは全文検索やフィルタ検索バー、監視とアラートといった機能ですが、その前提として ClickHouse 側のデータモデルが作り直されています。公式の説明では、トレース中心のモデルから「観測(observation)中心」のモデルへ移り、読み取り時の JOIN と重複排除を無くしたとあります2。
6 月の記事で、v3 の Langfuse が使う ClickHouse の traces / observations を直接 SQL で集計しました3。そこで書いたのは、エンジンが ReplacingMergeTree なので集計に FINAL が必要なこと、トークンとコストが Map 型で入っていることでした。v4 で「重複排除を無くした」なら、この集計 SQL はどう書き換わるのか。FINAL は不要になるのか。4 月の記事で「セルフホストでは v2 API を使えない」と書いた点4も、v4 で変わるはずです。
そこで、6 月からそのまま動かしていた v3 のスタックを実際に v4 へアップグレードし、ClickHouse の中で何が起きたかを確認しました。UI の新機能や API の速度比較は扱いません。テーブル定義、既存データの移り方、SQL の書き換えの 3 点に絞ります。
1.1. 結論(先出し)
- v3 の
traces/observations/scoresに代わり、events_full(全属性を 1 行に持つワイドテーブル)とevents_core(その軽量版)に変わる。トレース名・ユーザー ID・セッション ID・タグが各スパンの行に入り、tracesとの JOIN がなくなった -
events_fullのエンジンは v3 と同じReplacingMergeTree(event_ts, is_deleted)で、重複排除の仕組みはそのまま。ただし Langfuse 自身が v2 API で発行するクエリにはFINALが付いていなかった - 既存の 100 トレースは v3 のデータの変換(バックフィル)で
t-<trace_id>という ID のルートスパンに変換され、v3 のobservationsのidはそのまま v4 の表のspan_idになった。6 月の集計 SQL はFROMをevents_coreに変えただけで、件数・トークン・コストが一致した - アップグレードで詰まったのは環境変数だった。v3 の表に書き続ける移行モード(legacy)を 1 変数だけで指定すると、OpenTelemetry 側のデフォルト値と両立せず worker が起動しない。公式ガイドは 4 変数をセットで示している。移行期間用のステージング表は Python SDK 4.9 経由の送信では使われなかった
1.2. 検証ゴール
| # | 確かめること | 確認できれば OK の条件 |
|---|---|---|
| 1 | v3 から v4 で ClickHouse のテーブル構成がどう変わるか | アップグレード前後の SHOW TABLES と SHOW CREATE TABLE の差分で、増えた表・消えた表と、v4 の表のエンジン・ORDER BY が読める |
| 2 | 既存データ(v3 で保存した 100 件)が新モデルにどう移るか | v3 の 100 件が events_core に載り、トレースにあたるルートスパンが付いた状態で、6 月と同じ件数・コストの集計値が出る |
| 3 | 6 月の集計 SQL が v4 でどう書き換わるか | v3 向けの SQL と v4 向けの SQL を並べて、FINAL と JOIN の要否が分かる。Langfuse 自身が発行するクエリの書き方も確認できる |
2. 検証環境
| 項目 | 値 |
|---|---|
| Langfuse(アップグレード前) | 3.169.0(4 月に langfuse/langfuse:3 で起動したまま) |
| Langfuse(v3 最新) | 3.225.7 |
| Langfuse(アップグレード後) | 4.30.0(langfuse/langfuse:4 / langfuse/langfuse-worker:4) |
| ClickHouse | 26.3.30.9(単一ノード。タグを 26.3 に固定) |
| PostgreSQL / Redis | 17.9 / 7.4.8 |
| 構成 | Docker Compose(公式の docker-compose.yml を元にした 6 コンテナ。web / worker / ClickHouse / PostgreSQL / Redis / MinIO) |
| Python SDK | langfuse 4.9.0 |
| 既存データ |
observations 120 行(うち 6 月の記事で入れた Gemini 実呼び出し 100 行)、traces 108 行 |
v4 は ClickHouse 25.12 以上を要求します(推奨は 26.4)5。手元の compose は ClickHouse のイメージにタグを付けていなかったため、コンテナを作り直すたびに新しいバージョンが入り、確認した時点で 26.3 系になっていました。要件は偶然満たしていましたが、作業のたびに前提が変わらないよう、作業前にタグを 26.3 に固定しています。
公式は「ClickHouse スキーマは安定した API 契約ではない」と明記しており、メジャーアップグレードや性能改善でテーブル・列・重複排除の挙動が変わりうるとしています6。本記事の DDL やクエリ結果は上の表のバージョンで観測したものです。
3. v4 のデータモデル
実機で確認する前に、上流リポジトリのマイグレーション SQL7 と、アップグレード後に取得した SHOW CREATE TABLE から、v4 のデータモデルを整理します。v3 側の定義は、アップグレード前に同じ方法で取得した SHOW CREATE TABLE を使います。
3.1. 3 つの表から、1 つのワイドテーブルとその軽量版へ
v3 では、トレース単位の属性を traces、スパンや LLM 呼び出しを observations、評価スコアを scores に分けて持っていました。トレース一覧に「ユーザー ID で絞ってコストを合計する」ような表示を出すには、2 つの表を突き合わせる必要があります。
v4 では、スパン 1 行にトレースの属性まで含めて保存する events_full が中心です。トレースは独立した行ではなく、type = 'SPAN' のルートスパンとして同じ表に入ります。属性を各行にコピーして 1 表で読めるようにした、列の多いワイドテーブル(いわゆる大福帳型)です。
もう 1 つの events_core は events_full の軽量版で、マテリアライズドビュー events_core_mv が events_full に入った行の入出力を 200 文字に切り詰めて書き込みます。読む側から見ると、v4 のスパンは events_full(全文)と events_core(一覧・集計用)の 2 つの表で読めます。
3.2. events_full の定義
アップグレード後に取得した SHOW CREATE TABLE default.events_full から、要点を抜き出します。
CREATE TABLE default.events_full
(
`project_id` String,
`trace_id` String,
`span_id` String,
`parent_span_id` String,
`start_time` DateTime64(6),
`end_time` Nullable(DateTime64(6)),
`name` String,
`type` LowCardinality(String),
`environment` LowCardinality(String) DEFAULT 'default',
-- トレース単位の属性が各行に入る
`trace_name` String,
`user_id` String,
`session_id` String,
`tags` Array(String),
-- トークンとコストは v3 と同じ Map
`usage_details` Map(LowCardinality(String), UInt64),
`cost_details` Map(LowCardinality(String), Decimal(18, 12)),
`calculated_total_cost` Decimal(18, 12) MATERIALIZED arraySum(mapValues(mapFilter(...))),
`total_cost` Decimal(18, 12) ALIAS cost_details['total'],
-- 入出力
`input` String CODEC(ZSTD(3)),
`output` String CODEC(ZSTD(3)),
-- メタデータは Map ではなく並行する 2 本の配列
`metadata_names` Array(String),
`metadata_values` Array(String),
`event_ts` DateTime64(6),
`is_deleted` UInt8,
-- 全文検索用のインデックス
INDEX idx_fts_input_low lower(input) TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 100000000,
INDEX idx_fts_output_low lower(output) TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 100000000,
INDEX idx_fts_metadata_values metadata_values TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 100000000,
INDEX idx_ngram_metadata_values arrayStringConcat(metadata_values) TYPE ngrambf_v1(4, 32000, 3, 0) GRANULARITY 2,
...
)
ENGINE = ReplacingMergeTree(event_ts, is_deleted)
PARTITION BY toYYYYMM(start_time)
PRIMARY KEY (project_id, toStartOfMinute(start_time), xxHash32(trace_id))
ORDER BY (project_id, toStartOfMinute(start_time), xxHash32(trace_id), span_id, start_time)
SAMPLE BY xxHash32(trace_id)
SETTINGS index_granularity_bytes = '64Mi', merge_max_block_size_bytes = '64Mi',
enable_block_number_column = 1, enable_block_offset_column = 1, ...
アップグレード前の observations と比べて変わった点を表にします。
| 観点 | v3 observations
|
v4 events_full
|
|---|---|---|
| エンジン | ReplacingMergeTree(event_ts, is_deleted) |
ReplacingMergeTree(event_ts, is_deleted) |
| ORDER BY | (project_id, type, toDate(start_time), id) |
(project_id, toStartOfMinute(start_time), xxHash32(trace_id), span_id, start_time) |
| トレース属性 |
traces 表に別置き |
trace_name / user_id / session_id / tags を各行に持つ |
| トークン・コスト |
usage_details / cost_details の Map |
同じ Map。加えて calculated_*_cost の MATERIALIZED 列 |
| メタデータ | Map(LowCardinality(String), String) |
metadata_names と metadata_values の並行配列。値には text と ngram のインデックス |
| 入出力のインデックス | なし(bloom_filter は id / trace_id / metadata のみ) |
text インデックス(全文検索) |
| 時刻精度 | DateTime64(3) |
DateTime64(6) |
ORDER BY から type が消え、toStartOfMinute(start_time) と xxHash32(trace_id) が入りました。開始時刻が同じ 1 分の枠に入る、同じトレースのスパンがディスク上で隣り合う並びになります。トレース詳細の表示を速くするねらいと考えられます。公式ガイドは ClickHouse 25.12 以上を要求する理由として、軽量 UPDATE・JSON 型・全文検索の 3 つを挙げています5。text インデックスはその全文検索にあたる機能で、ClickHouse 25.12 でベータ、26.2 で正式になりました89。
一方でエンジンは変わっていません。公式は events_full を「不変(immutable)の全属性イベント表」と説明しています5。しかし DDL 上は v3 と同じ ReplacingMergeTree です。event_ts の新しい行で上書きし、is_deleted で削除を表す仕組みはそのままです。読み取り時に重複排除が必要かどうかは 5.4 章で確かめます。
3.3. events_core とマテリアライズドビュー
events_core は events_full と同じ列・同じ ORDER BY の表です。違いは input / output に圧縮コーデックの指定と text インデックスが無いことと、provided_model_name などに bloom_filter インデックスが追加されていることです。この表に events_core_mv が次のように入出力を切り詰めて書き込みます。
CREATE MATERIALIZED VIEW default.events_core_mv TO default.events_core AS
SELECT
project_id, trace_id, span_id, parent_span_id, start_time, end_time, name, type, ...
leftUTF8(input, 200) AS input,
leftUTF8(output, 200) AS output,
metadata_names,
arrayMap(v -> leftUTF8(v, 200), metadata_values) AS metadata_values,
...
FROM default.events_full
一覧画面や集計は events_core を読み、プロンプトと応答の全文が必要な詳細表示だけ events_full を読む分担と考えられます。5.4 章で確認した v2 API のクエリは events_core を読んでいました。
4. アップグレードで ClickHouse に起きたこと
公式ガイド5の手順に沿って進めました。v4 には書き込み先を決める環境変数 LANGFUSE_MIGRATION_V4_WRITE_MODE があり、v3 の表だけに書く legacy、v3 と v4 の両方に書く dual、v4 の表だけに書く events_only の 3 つの値を取ります。今回は v3 最新化のあと、legacy、dual、events_only の順に切り替え、各段階で SHOW TABLES と SHOW CREATE TABLE を保存しました。
4.1. 前提の確認
公式ガイドは、v3 の最新版へアップグレードしてバックグラウンドマイグレーションを完了させたうえで、次のクエリが 0 行を返すことを求めています。
SELECT name, failed_at, failed_reason
FROM background_migrations
WHERE finished_at IS NULL;
3.169.0 を 3.225.7 にしてこのクエリを実行すると、5 行が返りました。すべて 20260701_v4_step_1 から _5 という名前で、v4 のバックフィル用の行です。v3 の worker は「No background migrations to run」と記録しており、この 5 行は v3 では実行されません。v4 以外の行が 0 であることを確認して先へ進みました。
公式ガイドは v4 で ClickHouse ユーザーに DROP VIEW と ALTER の権限、system.parts などシステム表の読み取り権限を求めています。公式 compose のデフォルトユーザーには ON *.* の包括的な GRANT が付いており、追加作業はありませんでした。
4.2. legacy モードでアップグレードする
まず LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy を compose の共通環境変数(web と worker が同じブロックを参照する)に追加し、イメージを :4 に変えて起動しました。
web コンテナは起動し、Prisma のマイグレーションと ClickHouse のマイグレーション 38 から 48 を適用しました。SHOW TABLES の前後差分は次のとおりです。
- dataset_run_items
- event_log
- project_environments
+ events_core
+ events_core_mv
+ events_full
+ observations_batch_staging
消えた 3 表は、いずれも v3 の途中で後継の表に置き換わっていたものです5。増えた 4 つのうち observations_batch_staging は移行期間だけ使うステージング表で、3 分単位のパーティションに TTL 48 時間を付けた ReplacingMergeTree です。PostgreSQL 側でも traces / observations / scores / dataset_run_items が消え、evaluators 関連の 4 表が増えました。v3 の observations 120 行と traces 108 行はそのまま残り、events_full と events_core は 0 行です。
一方、worker コンテナは起動直後に停止し、再起動を繰り返しました。原因は環境変数を 1 つしか指定していなかったことで、公式ガイドが legacy モード用に示す 4 変数をそろえる必要がありました。詳しくは付録に書きます。スキーマの変更は web 側で実行されるため、worker が停止していても ClickHouse の DDL は v4 になります。
4.3. dual モードでバックフィル
環境変数を WRITE_MODE=dual と NATIVE_OTEL_BEHAVIOUR=dual_write に変えて再起動すると、web と worker がそろって起動しました。バックグラウンドの履歴バックフィル(LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL、デフォルト true)が worker で始まり、4.1 章で見た 5 行が順に処理されました。
| ステップ | 内容 | 結果 |
|---|---|---|
| v4_step_1 |
traces からルートスパンを作る |
完了 |
| v4_step_2 |
observations を (project_id, trace_id, id) 順に並べ直したスクラッチ表 observations_pid_tid_sorting を作る |
完了 |
| v4_step_3 | スクラッチ表と traces を突き合わせて events_full に書く |
完了 |
| v4_step_4 | データセット実行の情報を付ける | 対象 0 件で完了 |
| v4_step_5 | スクラッチ表を DROP する | 未実行(別の環境変数で有効化する設計) |
完了後の events_full と events_core は 228 行で、traces 108 行から作ったルートスパンと observations 120 行の合計と一致します。step_5 は LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLES=true を指定しないと実行されない設計で(公式ガイドの環境変数一覧でデフォルト false)5、スクラッチ表 120 行はそのまま残りました。step_1 から step_3 の所要は worker のログにいずれも「2.00 分」と記録されましたが、チャンクの開始時刻が 1 分刻みで並んでおり、データ量ではなく処理の待ち間隔で決まった値と考えられます。大規模なデータでの所要時間はこの値から見積もれません。
dual モードのまま、SDK 4.9.0 で 10 トレースを送って書き込み先を見ました。observations と events_full に即時に 20 行が入り、observations_batch_staging は 0 行のままでした。公式ガイドは、対応 SDK(Python 4.7.0 以上 / JS 5.4.0 以上)はトレース属性をクライアント側で付けてv4 の表に直接書き、それより古い SDK からの書き込みが v3 の表とステージング表に入って worker が v4 の表へ転記するとしています5。今回の 4.9.0 は前者にあたり、観測結果はこの説明と一致します。ステージング表を経由する経路は、手元に古い SDK がないため観測できていません。
4.4. events_only へ切り替える
最後に WRITE_MODE=events_only と NATIVE_OTEL_BEHAVIOUR=direct にして再起動し、もう 10 トレースを送りました。events_full と events_core には 20 行が入り、observations は増えませんでした。v3 の表への書き込みが止まった状態です。公式ガイドはこの時点を「後戻りできない区切り」としており、v3 の表への書き込みが無いため v3 へ戻すと以後のデータが失われます5。
5. v3 のデータは v4 の表でどう見えるか
5.1. トレースがルートスパンになった
6 月に入れた 100 件(送信時に environment を mokumoku にしたもの)を events_core で見ると、200 行になっています。type の内訳は SPAN 100 と GENERATION 100 です。
SELECT span_id, parent_span_id, type, name, trace_name
FROM default.events_core FINAL
WHERE environment = 'mokumoku' ORDER BY start_time LIMIT 2;
| span_id | parent_span_id | type | name | trace_name |
|---|---|---|---|---|
5a9ba3af12223051 |
t-504cee41438b2ddc98856b4c02483214 |
GENERATION | summarize | summarize |
t-504cee41438b2ddc98856b4c02483214 |
(空) | SPAN | summarize | summarize |
v3 の traces の 1 行が、span_id を t- にトレース ID を続けた値、parent_span_id を空にしたルートスパンになっています(2 行の start_time は同じで、出力順は上のとおりでした)。v3 の observations の行は、id がそのまま span_id に、trace_id から作ったルートスパンの ID が parent_span_id に入っています。v3 の observations と span_id で突き合わせた先頭 3 件はすべて一致し、GENERATION の行数 100 も旧表と同じでした。trace_name は旧 traces.name から各行にコピーされています。
5.2. 6 月の集計 SQL を v4 の表で実行する
6 月の記事で使った SQL は、FROM default.observations FINAL を FROM default.events_core FINAL に変えただけで、列名の変更なしにそのまま実行できました。結果を並べます。
| 集計 | v3 observations
|
v4 events_core
|
|---|---|---|
| GENERATION の行数 | 100 | 100 |
トレース数(uniqExact(trace_id)) |
100 | 100 |
| モデル数 / 処理種別数 | 1 / 5 | 1 / 5 |
| 合計コスト(USD) | 0.023882 | 0.0238825 |
| 入力トークン / 出力トークン | 4264 / 15211 | 4264 / 15211 |
| 処理種別ごとのコスト 1 位 | tool-call 0.006693 | tool-call 0.0066925 |
| 処理種別ごとのコスト 2 位 | summarize 0.005309 | summarize 0.005309 |
| レイテンシ最大 | 2522 ms | 2522 ms |
合計コストの桁数の差は、v3 側の SQL で round(…, 6) していたためで、値は同じです。usage_details['input'] のような Map の取り出し方も、total_cost の列名も v3 のまま使えました。total_cost は v4 では cost_details['total'] の ALIAS 列になっています。
v3 で traces との JOIN が必要だった集計は、v4 では 1 表で書けます。
-- v4: トレース名・ユーザー・セッション別のコスト。JOIN なし
SELECT trace_name, user_id, session_id, sum(total_cost) AS cost_usd, count() AS calls
FROM default.events_core FINAL
WHERE environment = 'mokumoku' AND is_deleted = 0 AND type = 'GENERATION'
GROUP BY ALL ORDER BY cost_usd DESC;
今回のデータは SDK でユーザー ID とセッション ID を付けていないため空欄ですが、列は各行にあります。
5.3. FINAL の有無で値が変わるか
events_core に対して 5.2 章と同じ 4 つの件数(行数・トレース数・モデル数・処理種別数)を FINAL あり・なしで集計すると、どちらも 100 / 100 / 1 / 5 で一致しました。バックフィル直後で更新が無いデータなので、未マージの重複行が無い状態です。これは「FINAL が不要になった」という意味ではなく、このデータでは差が出なかったという結果です。
5.4. Langfuse 自身が発行するクエリ
Langfuse が events_core をどう読んでいるかを system.query_log から取得しました。v2 の Observations API(5.5 章)を呼んだ直後に記録されたクエリは、events_core を別名 e で読み、span_id を id として返す SELECT で、FINAL は付いていませんでした。
SELECT e.span_id AS id, e.trace_id AS trace_id, e.start_time AS start_time,
e.end_time AS end_time, e.project_id AS project_id,
e.parent_span_id AS parent_observation_id, e.type AS type, e.name AS name, ...
FROM events_core e
...
read_rows は 289、所要は 8 ms と 15 ms でした。取得できた Langfuse 発行の SELECT のうち events_core を読んでいたのは、この v2 API の 2 本と、バックグラウンドで動く実験データ処理の 2 本で、いずれも FINAL なしでした。根拠は合計 4 本のクエリで、UI にログインして画面を操作したときのクエリは今回は取得していません。
5.5. v2 API がセルフホストで使える
4 月の記事と同じ 2 つの呼び出しを、v4(events_only)に対して実行しました。
| 呼び出し | 4 月(v3.169.0) | 今回(v4.30.0) |
|---|---|---|
langfuse.api.observations.get_many()(v2) |
失敗 | 成功(5 件取得) |
langfuse.api.legacy.observations_v1.get_many()(旧) |
成功 | NotFoundError |
REST で直接呼ぶと、GET /api/public/v2/observations が 200、GET /api/public/observations と GET /api/public/traces が 404 で、本文は「This endpoint is not available on deployments running in Langfuse v4 events_only mode」でした。公式ガイドのとおり、events_only に切り替えた時点で旧 API は使えなくなります5。
5.6. ディスク使用量
バックフィル完了直後(追加送信前)の各表のサイズです。
| 表 | 行数 | ディスク |
|---|---|---|
traces(v3 の表) |
108 | 32.90 KiB |
observations(v3 の表) |
120 | 48.27 KiB |
events_full(v4 の表) |
228 | 208.38 KiB |
events_core(v4 の表) |
228 | 119.03 KiB |
observations_pid_tid_sorting(スクラッチ) |
120 | 48.39 KiB |
200 行規模なので比率に意味はありませんが、v3 の 2 表の合計 81 KiB に対し、v4 の 2 表とスクラッチ表で 376 KiB が加わりました。公式ガイドはバックフィル時に「現在のデータ量の約 3 倍」の空きを求めており、その理由をデータが新しい表と中間形式へコピーされるためとしています5。
6. 考察
6.1. 「JOIN と重複排除を無くした」で何が変わったか
公式は v4 の狙いを、読み取り時の JOIN と重複排除を無くすことだと説明しています2。JOIN については、3.2 章の DDL のとおりトレース属性が各行に入り、5.2 章で traces なしに書けることを確かめました。
重複排除については、エンジンが ReplacingMergeTree のままなので、重複排除の仕組みはそのままです。変わったのは読み取り側のクエリで、取得できた Langfuse 自身のクエリ 4 本(v2 API とバックグラウンド処理)は FINAL を付けていませんでした。書き込みを「新しい行の追加」に寄せ、更新頻度の高い属性を減らしたことで、未マージの重複を許容できる範囲に収めていると考えられます。更新が多い環境で同じ結果になるかは確かめていません。自分で events_core を集計するときは、v3 と同じく FINAL を付けるのが安全です。
6.2. アップグレード作業で詰まった点と事前確認
アップグレードそのものは、公式ガイドの手順で ClickHouse のデータを失うことなく完了しました。詰まった点は 2 つで、どちらも環境変数でした。
- legacy モードは公式ガイドの 4 変数をセットで指定する。
WRITE_MODEだけ追加すると OpenTelemetry 側のデフォルトdirectと両立せず worker が起動しない。web は起動してスキーマ移行が完了する - 事前確認のクエリは、v3 の最新版でも v4 用のバックフィル行が未完了として返る。v4 以外の行が 0 かどうかで判断する
ClickHouse のイメージにタグを付けていなかったため、要件を満たすバージョンが偶然入っていました。要件を満たさないバージョンが入ることもありうるので、メジャーアップグレードの前にはデータベース側のタグを固定してから始めるのがよいと考えます。
v3 の表とスクラッチ表は自動では消えません。公式ガイドは、events_only への切り替えが済み、バックフィルの完了か保持期間ぶんの dual 書き込みによって必要な履歴が新モデルで見えることを確認したうえで、TRUNCATE TABLE traces; TRUNCATE TABLE observations; を手で実行してよいとしています。v3 の表は v3 の読み取り経路へ戻す最後の手段でもあるため、消すと戻れなくなる点も併記されています5。スクラッチ表の DROP も別の環境変数で有効化する設計でした。ディスク使用量を減らすには自分で後片付けが必要です。
6.3. 手元で確かめられなかったこと
-
observations_batch_stagingを経由する書き込みは観測できなかった。旧 SDK からの取り込みを受ける環境では挙動が違う可能性がある - バックフィルの所要時間とディスクの増え方は 200 行規模の値で、大規模データの見積もりには使えない
- UI 操作で発行されるクエリは取得していない。
FINALの有無を見たのは v2 API とバックグラウンド処理の範囲
7. まとめ
| 確かめたこと | 結果 |
|---|---|
| テーブル構成の変化 |
events_full / events_core / events_core_mv / observations_batch_staging が増え、event_log ほか 2 表が消えた。エンジンは ReplacingMergeTree のまま、ORDER BY とトレース属性の持ち方が変わった |
| 既存データの移り方 | 100 トレースが t-<trace_id> のルートスパンになり、v3 の observations.id が span_id に引き継がれた。件数・トークン・コストは v3 の表と一致 |
| SQL の書き換え |
FROM を events_core に変えるだけで 6 月の SQL が実行できた。トレース属性の JOIN は不要になり、Langfuse 自身のクエリは FINAL なし |
Langfuse の ClickHouse を直接読む使い方は、v4 でも変わらずできます。読むべき表が events_core に変わり、トレース属性のための JOIN が無くなった分、SQL は短くなりました。自分で集計するときの FINAL は、v3 と同じ扱いにしておくのがよさそうです。
8. 付録:legacy モードで worker が起動しなかった件
4.2 章で LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy だけを追加して起動したとき、worker は次のエラーで停止しました。
Error: Invalid V4 config: LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=direct requires
LANGFUSE_MIGRATION_V4_WRITE_MODE in {dual, events_only}.
Direct OTel writes target events_full, which is not read in legacy mode.
OpenTelemetry 経由の書き込み先を決める LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR のデフォルトが direct(events_full に直接書く)で、legacy とは両立しないという内容です。公式ガイドは legacy モードを次の 4 変数のセットで示しています5。
LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy
LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=dual_write
LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN=false
LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL=false
web は同じ環境変数で起動してスキーマ移行を完了しているので、起動時の設定検査は web と worker で異なると考えられます。4.3 章では WRITE_MODE=dual と NATIVE_OTEL_BEHAVIOUR=dual_write の組み合わせにして解消しました。
参考
-
Langfuse v4.0.0 リリースノート(GitHub Releases。2026-07-29 公開。全文検索・フィルタ検索バー・監視とアラート・Observations API v2 がセルフホストで使えるようになった旨) ↩
-
Upcoming architecture changes: Simplify Langfuse for Scale (v4)(GitHub Discussions。観測中心のデータモデルへ移り、読み取り時の JOIN と重複排除を無くすという設計の説明) ↩ ↩2
-
Langfuse の裏側 ClickHouse を直接確認して、LLM トレースのコスト・レイテンシを自然言語で分析してみた(筆者の 2026-06 記事。v3 の
observationsを直接集計) ↩ -
Langfuse Python SDK v4 の公開 REST API v2 をセルフホストと Cloud で叩き比べてみた(筆者の 2026-04 記事。v3 セルフホストでは v2 API が使えなかった) ↩
-
Migrate Langfuse v3 to v4 (self-hosted)(Langfuse 公式のアップグレードガイド。前提バージョンとその理由、write mode と環境変数、作成・削除される表、仮想ルートスパン、バックフィルとディスク 3 倍、ロールバックの区切り) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
ClickHouse (self-hosted)(Langfuse 公式。v4 の ClickHouse 要件と「スキーマは安定した API 契約ではない」の記述) ↩
-
langfuse/langfuse の ClickHouse マイグレーション SQL(GitHub。0038 から 0041 が v4 の表と MV の作成、0042 と 0043 が列とインデックスの追加、0044 から 0046 が v3 の表の DROP) ↩
-
ClickHouse Release 25.12(ClickHouse 公式ブログ。text インデックスがベータになったリリース) ↩
-
ClickHouse Release 26.2(ClickHouse 公式ブログ。text インデックスが正式機能になったリリース) ↩

