Langfuse on ClickHouse, without Langfuse
Langfuse (open-source LLM observability) keeps its OLTP state in Postgres, but every observation and score lands in ClickHouse. This lab loads a snapshot of that ClickHouse database into a plain ClickHouse server and runs the ClickHouse SQL from the langfuse-hols workshop on it. You need one container. Langfuse, Postgres and Python are not needed.
Verified end to end on 2026-10-06 against ClickHouse 26.8.18.2: tools/hol run passes all
four SQL files. The snapshot itself comes from Langfuse 4.52.0, SDK 4.17.0 and ClickHouse
26.8.18.2.
What is in the snapshot
00-data.sql recreates Langfuse's ClickHouse database default:
- every table and view exactly as Langfuse's migrations created them (no DDL changes), and
- the raw rows, read without
FINAL.
It is generated by export_snapshot.py from one run of the langfuse-hols v4
track at commit c7901ec:
- OSS mode, no model key, so every model call is simulated;
langfuse-ee02 seeded 40 traces;langfuse-eval01 seeded 20 more, and 02–06 added prompts, a dataset, two experiments, a rubric judge and an annotation queue.
| Table | Rows | What it holds |
|---|---|---|
events_full |
424 | One wide row per observation: 240 SPAN, 104 GENERATION, 80 EVALUATOR. 101 of them are trace root rows (is_app_root) |
events_core |
424 | The same rows with input, output and metadata truncated. In Langfuse events_core_mv fills it; the snapshot loads its rows directly and creates the MV last |
scores |
212 | Every quality signal. All of them have source = 'API' (see Limits) |
blob_storage_file_log, schema_migrations |
212, 100 | Langfuse bookkeeping |
traces, observations |
0 | v3 tables: still created by the migrations, but v4 no longer writes to them. 01-explore.sql shows this |
dataset_run_items_rmt, observations_batch_staging, observations_pid_tid_sorting |
0 | Empty in this run |
A round trip proves the copy is exact. 00-data.sql was loaded into a fresh
clickhouse/clickhouse-server:26.8.18.2, and all 14 objects matched the source on count() and
sum(cityHash64(toString(tuple(*)))). As a negative check, a deliberately modified copy showed
DIFF, and reloading the file restored the match.
Files
| File | What it does |
|---|---|
00-data.sql |
Generated snapshot: DDL, raw rows, materialized views created last (1.5 MB) |
01-explore.sql |
Langfuse's tables, engines and sort keys; a trace as its root row (langfuse-ee/03) |
02-analytics.sql |
Cost, latency, tokens and quality as plain ClickHouse SQL (langfuse-ee/04) |
03-scores.sql |
Scores joined to their traces' root rows (langfuse-eval/07) |
export_snapshot.py |
Regenerates 00-data.sql from a running langfuse-hols v4 stack |
The three SQL files are copies of the langfuse-hols files at c7901ec. Only their run
instructions changed.
Run
tools/hol run usecase/langfuse-on-clickhouse
That starts a throwaway clickhouse/clickhouse-server:26.8.18.2 and runs 00…03 in order. Add
--keep to leave it up and query it yourself:
docker exec -it hol-usecase-langfuse-on-clickhouse clickhouse-client
Limits
- The schema is Langfuse's internal detail, not an API. It matches Langfuse 4.52.0. Other
versions can rename columns or tables (v3 → v4 moved everything to
events_*). - All 212 scores are
source = 'API'.03-scores.sqlis written for three sources:API,EVALandANNOTATION. In this offline run every score came in through the SDK/API, including the rubric judge (05) and the annotation-queue demo scores (06).EVALneeds Langfuse's managed LLM judge, which needs a model connection. FINALchanges nothing here. Every row has its own sorting key and none is marked deleted. For example,events_fullhas 424 rows with and withoutFINAL. The SQL still usesFINAL, as it should on a live Langfuse, but the snapshot has no row versions for it to collapse.- Simulated data. No model was called, so costs, latencies and outputs come from the
generator, not from a real LLM. The users (
user_001…) are synthetic. The only key in the data is the workshop's public placeholderpk-lf-workshop-public, iningestion_api_key.
Regenerate the snapshot
- Bring up the langfuse-hols v4 stack. Host ports 8123 and 9000 must be free, so stop a ClickStack or any other local ClickHouse first.
- Run the seed commands listed in the header of
00-data.sql, withANTHROPIC_API_KEYempty. - Run the regenerate command from the same header, then
tools/hol runagain.
The generator writes byte-identical output for the same data and --date.
Related
- langfuse-hols: the live versions of these labs,
with Langfuse itself: v4
langfuse-ee(OSS 01–04, Enterprise 05–11) andlangfuse-eval.
Langfuse(오픈소스 LLM 관측 도구)는 OLTP 상태를 Postgres에 두지만, 모든 observation과 score는 ClickHouse에 쌓입니다. 이 실습은 그 ClickHouse 데이터베이스의 스냅샷을 일반 ClickHouse 서버에 올리고, langfuse-hols 워크숍의 ClickHouse SQL을 그 위에서 실행합니다. 컨테이너 하나만 있으면 됩니다. Langfuse, Postgres, Python은 필요 없습니다.
2026-10-06 ClickHouse 26.8.18.2에서 end-to-end 검증: tools/hol run이 SQL 파일 네 개를
모두 통과합니다. 스냅샷 자체는 Langfuse 4.52.0, SDK 4.17.0, ClickHouse 26.8.18.2에서
만들었습니다.
스냅샷에 든 것
00-data.sql은 Langfuse의 ClickHouse 데이터베이스 default를 다시 만듭니다.
- 모든 테이블과 뷰를 Langfuse 마이그레이션이 만든 그대로 만듭니다. DDL은 바꾸지 않았습니다.
- 원본 행을
FINAL없이 그대로 넣습니다.
이 파일은 export_snapshot.py가 langfuse-hols v4 트랙(커밋 c7901ec)을
한 번 실행한 결과에서 생성했습니다.
- OSS 모드, 모델 키 없음. 모델 호출은 모두 시뮬레이션입니다.
langfuse-ee02로 trace 40개를 넣었습니다.langfuse-eval01로 trace 20개를 더 넣고, 02–06으로 프롬프트, 데이터셋, 실험 두 번, 규칙 기반 평가자, 어노테이션 큐를 추가했습니다.
| 테이블 | 행 | 내용 |
|---|---|---|
events_full |
424 | observation 하나당 넓은 행 하나: SPAN 240, GENERATION 104, EVALUATOR 80. 그중 101개가 trace의 root 행(is_app_root) |
events_core |
424 | 같은 행에서 input, output, metadata를 잘라 낸 사본. Langfuse에서는 events_core_mv가 채우지만, 스냅샷은 행을 직접 넣고 MV는 마지막에 만듦 |
scores |
212 | 모든 품질 신호. 전부 source = 'API' (한계 참고) |
blob_storage_file_log, schema_migrations |
212, 100 | Langfuse 내부 기록 |
traces, observations |
0 | v3 테이블. 마이그레이션이 여전히 만들지만 v4는 쓰지 않음. 01-explore.sql이 이를 보여 줌 |
dataset_run_items_rmt, observations_batch_staging, observations_pid_tid_sorting |
0 | 이번 실행에서는 비어 있음 |
사본이 원본과 똑같다는 것은 왕복 검증으로 확인했습니다. 새
clickhouse/clickhouse-server:26.8.18.2에 00-data.sql을 올리자, 14개 객체 모두 count()와
sum(cityHash64(toString(tuple(*))))가 원본과 일치했습니다. 반대 확인으로 일부러 고친 사본은
DIFF가 나왔고, 파일을 다시 올리자 다시 일치했습니다.
파일
| 파일 | 하는 일 |
|---|---|
00-data.sql |
생성된 스냅샷: DDL, 원본 행, 마지막에 만드는 materialized view (1.5 MB) |
01-explore.sql |
Langfuse의 테이블, 엔진, 정렬 키. trace가 곧 root 행이라는 것 (langfuse-ee/03) |
02-analytics.sql |
비용, 지연, 토큰, 품질을 일반 ClickHouse SQL로 (langfuse-ee/04) |
03-scores.sql |
score를 해당 trace의 root 행과 조인 (langfuse-eval/07) |
export_snapshot.py |
실행 중인 langfuse-hols v4 스택에서 00-data.sql을 다시 생성 |
SQL 파일 세 개는 langfuse-hols c7901ec의 파일을 그대로 복사했고, 실행 안내만 바꿨습니다.
실행
tools/hol run usecase/langfuse-on-clickhouse
임시 clickhouse/clickhouse-server:26.8.18.2를 띄우고 00…03을 순서대로 실행합니다.
--keep을 붙이면 컨테이너가 남아 직접 쿼리할 수 있습니다.
docker exec -it hol-usecase-langfuse-on-clickhouse clickhouse-client
한계
- 스키마는 API가 아니라 Langfuse의 내부 구현입니다. Langfuse 4.52.0 기준입니다. 다른
버전에서는 컬럼이나 테이블 이름이 바뀔 수 있습니다(v3 → v4에서 모두
events_*로 옮겨감). - score 212개가 모두
source = 'API'입니다.03-scores.sql은API,EVAL,ANNOTATION세 출처를 염두에 두고 썼습니다. 이번 오프라인 실행에서는 규칙 기반 평가자(05)와 어노테이션 큐 데모 점수(06)를 포함해 모든 score가 SDK/API로 들어왔습니다.EVAL은 Langfuse의 관리형 LLM judge가 필요하고, 그러려면 모델 연결이 있어야 합니다. - 여기서는
FINAL이 결과를 바꾸지 않습니다. 모든 행의 정렬 키가 서로 다르고 삭제 표시된 행도 없습니다. 예를 들어events_full은FINAL이 있든 없든 424행입니다. 실제 Langfuse에서는FINAL이 필요하므로 SQL에는 그대로 두었지만, 이 스냅샷에는 합칠 행 버전이 없습니다. - 시뮬레이션 데이터입니다. 모델을 호출하지 않았으므로 비용, 지연, 출력은 실제 LLM이 아니라
생성기가 만든 값입니다. 사용자(
user_001…)도 합성 값입니다. 데이터에 있는 키는ingestion_api_key컬럼의 워크숍 공개 키pk-lf-workshop-public하나뿐입니다.
스냅샷 다시 만들기
- langfuse-hols v4 스택을 띄웁니다. 호스트 포트 8123과 9000이 비어 있어야 하므로, ClickStack 같은 다른 로컬 ClickHouse가 떠 있으면 먼저 멈춥니다.
00-data.sql머리말에 적힌 시드 명령을ANTHROPIC_API_KEY를 비운 채 실행합니다.- 같은 머리말의 재생성 명령을 실행한 뒤
tools/hol run을 다시 돌립니다.
데이터와 --date가 같으면 생성기는 바이트 단위로 같은 파일을 씁니다.
관련 저장소
- langfuse-hols: Langfuse를 직접 띄우는 이 실습들의
원본. v4
langfuse-ee(OSS 01–04, Enterprise 05–11)와langfuse-eval.