REFERENCE · 2026-11

Langfuse v4 数据链路参考

上报 → 转存 → 处理 → 存储 → 导出,以及企业改造关注的三件事:客户端对接、脱敏、大体积数据。每条结论都标了源码位置,改造时以代码为准。

0 核对基准

对象版本说明
langfuse/langfuse6918181(2026-11-05),web 4.76.0服务端:web、worker、packages/shared、ClickHouse 迁移
langfuse/langfuse-python5.2.0客户端:请求头、属性名、传播、mask、媒体
官方文档v4 changelog(2026-08-17)、v3→v4 升级指南、OTel 迁移指南、导出文档、2026-03-10 博客与代码不一致处见 §13

1 架构与数据流

两个自有进程:Web(Next.js,界面与所有接口,含上报)和 Worker(Express,处理上报、评估、导出)。v4 没有新增服务。

存储内容
PostgreSQL ≥ 15用户、组织、项目、API 密钥、提示词、数据集、评估配置、媒体记录(media 表)
ClickHouse ≥ 25.12追踪数据(events_full / events_core)、评分(scores);v3 表 traces / observations 仍存在
Redis / Valkey ≥ 7.0BullMQ 队列,API 密钥与提示词缓存,双写游标
S3 / Blob原始上报事件(事件桶)、媒体与溢出字段(媒体桶)、导出文件
SDK / 埋点→ OTLP →Web 校验→ 整批 JSON →S3 事件桶+ 地址 →Redis 队列→Worker 下载·(脱敏)·转换·补全→ 批量 →ClickHouse events_full→ MV →events_core

2 客户端对接的六种情况

写入路径由 resolveOtelWritePath 决定,优先级:direct_header > direct_env > direct_scope > direct_org_cutoff(仅 Cloud)> dual。worker/src/queues/otelIngestionQueue.ts

#客户端请求头 / 识别路径(dual 模式 / events_only 模式)trace 属性注意
1新版 SDK:Python 5.x、JS 6.xx-langfuse-ingestion-version: 4 + sdk 名与版本直写 / 直写SDK 复制到每个 span要求 v4 服务端
2过渡 SDK:Python 4.x、JS 5.xsdk 名与版本(Python ≥ 4.0.0、JS ≥ 5.0.0 即直写)直写 / 直写Python ≥ 4.7.0、JS ≥ 5.4.0 才复制;更早只在根 span同时兼容 v3/v4 服务端
3旧 OTel SDK:Python 3.x、JS 4.x无版本头;作用域名含 langfuse默认(NATIVE_OTEL_BEHAVIOUR=direct)直写;设为 dual_write(仅 legacy/dual 模式可用)才走双写,约 10–15 min 延迟dual_write:作业从 traces 表补;direct:不补直写时子 span 按用户查不到
4非 OTel SDK(Python 2.x、JS 3.x)或自调 /api/public/ingestionREST 批量接口旧路径 / 只收 score-create,其余 400—升级时最容易漏
5第三方 OTel(OpenInference、Vercel AI SDK、框架自带)建议手动加版本头 4无头:dual 下走 dual;NATIVE_OTEL_BEHAVIOUR=direct(默认)则直写需自己写到每个 span未识别属性进 metadata.attributes
6企业 OTel Collector 转发透传 Authorization 与版本头同上游客户端可在采集器补资源属性采样不能丢根 span;同一 span 不要两路发送(读取不去重)
SDK 主版本与服务端:Python SDK v4 / JS SDK v5 可连 v3 和 v4 服务端;Python SDK v5 / JS SDK v6 需要 v4 服务端(官方升级指南)。

3 上报协议与属性

入口

项值源码
地址POST /api/public/otel/v1/tracesweb/src/pages/api/public/otel/v1/traces/index.ts
内容类型application/x-protobuf 或 application/json;其他 400web/src/server/otel/processOtelIngestion.ts
压缩Content-Encoding: gzip同上
认证Authorization: Basic base64(publicKey:secretKey)
体积LANGFUSE_OTEL_INGESTION_MAX_BODY_BYTES 默认 512 MiB,压缩前后都检查;超限 413 + Connection: close;> 16 MiB 记警告web/src/server/otel/otelRequestBody.ts
编号校验任一 span 缺 traceId/spanId 或集合不是数组 → 整批 400(常见:OTLP 日志打到 traces 端点)validateOtelSpanIds
版本头x-langfuse-ingestion-version:> 4 → 400;= 4 → 直写候选processOtelIngestion.ts

Python SDK 默认请求头

Authorization: Basic …
x-langfuse-sdk-name: python
x-langfuse-sdk-version: 5.2.0
x-langfuse-public-key: pk-lf-…
x-langfuse-ingestion-version: 4

langfuse/_client/span_processor.py

属性名(Langfuse 约定)

含义属性
trace 级(每个 span 都要带)langfuse.trace.name · user.id/langfuse.user.id · session.id/langfuse.session.id · langfuse.trace.tags · langfuse.trace.metadata.<key> · langfuse.version · langfuse.release · langfuse.environment
observationlangfuse.observation.type(span、generation、event、embedding、agent、tool、chain、retriever、guardrail、evaluator)· .input · .output · .metadata.<key> · .level · .status_message
生成类langfuse.observation.model.name · .model.parameters · .usage_details · .cost_details · .completion_start_time · .prompt.name · .prompt.version
实验langfuse.experiment.id · .name · .dataset.id · .item.id · .item.expected_output …
第三方约定(服务端识别)gen_ai.*(OTel GenAI)· ai.*(Vercel AI SDK)· llm.*(OpenInference)等

langfuse/_client/attributes.py · packages/shared/src/server/otel/OtelIngestionProcessor.ts

user_id / session_id 的查找顺序

  1. span 属性:langfuse.user.id、user.id、langfuse.observation.metadata.langfuse_user_id、langfuse.trace.metadata.langfuse_user_id、ai.telemetry.metadata.userId
  2. 资源属性:langfuse.user.id、user.id
  3. 最后:user.name(span,再资源)

session 还识别 gen_ai.conversation.id。服务端不会在批内从根 span 继承;资源属性对整个服务生效,可用于“一个服务 = 一个租户”。

SDK 传播

from langfuse import get_client, propagate_attributes
with propagate_attributes(user_id="u-42", session_id="s-7", tags=["prod"], metadata={"tenant": "acme"}):
    ...   # 这里创建的每个 span 都带上这些属性
# as_baggage=True:通过 OTel Baggage(HTTP 头)传给下游服务 —— 会发给所有下游,不要放敏感值

langfuse/_client/propagation.py

4 转存与队列

路径S3 对象键队列入队延迟
OTel{LANGFUSE_S3_EVENT_UPLOAD_PREFIX}otel/{projectId}/yyyy/mm/dd/hh/mm/{uuid}.json,内容为整批 resourceSpans 数组otel-ingestion-queue / secondary-otel-ingestion-queue0
旧 REST{PREFIX}{projectId}/{entityType}/{entityId}/{eventId}.json,同一实体的多次更新同目录ingestion-queue / secondary-ingestion-queue非评分 ≤ 5 s;UTC 23:45–00:15 用 LANGFUSE_INGESTION_QUEUE_DELAY_MS

OTel 队列任务只带指针:fileKey、authCheck.scope(projectId、orgId)、sdkName、sdkVersion、ingestionVersion、propagatedHeaders。分片键 {projectId}-{fileKey}。OtelIngestionProcessor.ts publishToOtelIngestionQueue · processEventBatch.ts · eventBucketPath.ts

事件桶加密:LANGFUSE_S3_EVENT_UPLOAD_SSE(AES256 / aws:kms)+ LANGFUSE_S3_EVENT_UPLOAD_SSE_KMS_KEY_ID。媒体桶有对应的 LANGFUSE_S3_MEDIA_UPLOAD_SSE*。

5 后台处理与写入路径

下载 S3→脱敏回调(EE,可选)→processToEvent→createEventRecord 补全→观测级评估调度→字段溢出(可选)→ClickhouseWriter
补全做法
提示词prompt.name + version → Postgres → prompt_id
模型provided_model_name → 价格表匹配 → model_id
用量未提供时按模型分词器计算 token
用量与成本客户端提供的保留在 provided_usage_details / provided_cost_details;补全结果写入 usage_details / cost_details
元数据flattenJsonToPathArrays → metadata_names[] / metadata_values[]

写入缓冲:LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=1000、…_WRITE_INTERVAL_MS=1000、…_MAX_ATTEMPTS=3。worker/src/services/IngestionService/index.ts · worker/src/env.ts

6 表结构

events_full(迁移 0039)

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_full_text_index = 1, …
组列类型 / 说明
标识project_id, trace_id, span_id, parent_span_idString;根 span 的 parent 为空串;历史 trace 为虚拟根 t-<trace_id>
时间start_time, end_time, completion_start_timeDateTime64(6)(v3 为 (3))
trace 属性trace_name, user_id, session_id, tags, release, version, environment直接在行上
状态name, type, level, status_message, is_app_root, bookmarked, publicbookmarked/public 用轻量更新
模型model_id, provided_model_name, model_parametersmodel_parameters 为 JSON 字符串
用量成本provided_usage_details, usage_details, provided_cost_details, cost_detailsMap(LowCardinality(String), UInt64 / Decimal(18,12))
派生calculated_input_cost, calculated_output_cost, calculated_total_costMATERIALIZED:按键名含 input/output 求和
total_costALIAS cost_details['total']
I/Oinput, output, input_length, output_lengthString ZSTD(3);长度 MATERIALIZED lengthUTF8
元数据metadata_names, metadata_values两个等长 Array(String)
工具tool_definitions, tool_calls, tool_call_namesMap / Array
实验experiment_id, experiment_name, experiment_item_* …
来源source, service_name/version, scope_name/version, telemetry_sdk_*, ingestion_api_key, ingestion_sdk_name/versionsource 取值含 otel-dual-write、ingestion-api-dual-write 等
存储blob_storage_file_path, event_bytes, created_at, updated_at, event_ts, is_deletedfile_path 指回 S3 原始文件

索引:bloom_filter(span_id、trace_id、user_id、session_id);text 索引 splitByNonAlpha(lower(input)、lower(output)、metadata_names、metadata_values);ngrambf(metadata 拼接值、lower(name)、lower(trace_name)、lower(user_id)、lower(session_id))。canonical/0039, 0043, 0049

events_core(0040)与 events_core_mv(0041)

结构相同;物化视图写入时 leftUTF8(input, 200)、leftUTF8(output, 200)、每个 metadata 值截到 200。查询写逻辑表 events,编译期路由:读到被截断列 → events_full,否则 events_core;短值等值/前缀的 metadata 过滤在 core 上也正确;events_truncated 强制 core。packages/shared/src/server/query-ast/eventsRouting.ts

元数据展平规则

输入namesvalues
{"tenant":"acme"}tenantacme
{"req":{"id":"r-1","retry":2}}req.id, req.retryr-1, 2(字符串)
{"flags":["a","b"]}flags["a","b"](整个数组 JSON)
{"pii":true,"note":null}pii, notetrue, ""

读取还原:mapFromArrays(arrayReverse(metadata_names), arrayReverse(metadata_values))。键名本身含点时与路径无法区分。packages/shared/src/server/otel/utils.ts · query-ast/compile.ts

7 格式转换总表

层编号时间input / outputmetadata其他
① OTel span(客户端)traceId 16 字节、spanId 8 字节startTimeUnixNano 整数属性中的 JSON 字符串langfuse.*.metadata.<k> 多个属性属性为 {key, value} 列表
② S3 原始文件protobuf 解析后的对象形式int64 → 十进制字符串同上同上不脱敏、不修改
③ EventInput32 / 16 位十六进制字符串ISO 字符串可为对象对象未识别属性 → metadata.attributes / resourceAttributes
④ EventRecordInsertType同上ClickHouse 时间字符串字符串names[] / values[]+ model_id、usage、cost、prompt_id、source、blob_storage_file_path
⑤ events_fullStringDateTime64(6)String(可选:溢出为媒体引用)Array(String) ×2Map / Decimal(18,12)
⑥ events_core同上同上≤ 200 字符每个值 ≤ 200MV 生成
⑦ API v2 / 导出id、trace_id…ISO;latency 秒或毫秒按字段组;core 表时为截断内容对象按 span 去重

8 大体积数据:七道关口

关口规则默认源码
① 客户端每批字节上限;媒体识别后抽走(data:*;base64、Anthropic {type: base64}、Gemini inline_data)LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES 未设则用导出器默认langfuse/_client/span_processor.py · _task_manager/media_manager.py
② 入口OTel 请求体上限;旧 REST 4.5 MB512 MiB;>16 MiB 警告otelRequestBody.ts · pages/api/public/ingestion.ts
③ 媒体上传POST /api/public/media 申请预签名 URL;sha256 去重(已存在则 URL 为空);键 {prefix}{projectId}/{mediaId}.{ext}单文件 LANGFUSE_S3_MEDIA_MAX_CONTENT_LENGTH = 1,000,000,000pages/api/public/media · server/media/mediaService.ts
④ 服务端抽取 / 字段溢出服务端媒体抽取;input/output/每个 metadata 值 > 阈值 → 上传媒体桶,原位替换为 @@@langfuseMedia:type=text/plain|id=…|source=field_size_limit@@@;上传失败保留原值LANGFUSE_OTEL_MEDIA_UPLOAD_ENABLED=false;LANGFUSE_OBSERVATION_FIELD_OVERFLOW_ENABLED=false;阈值 2 MiBfeatures/observation-field-overflow
⑤ 写入兜底ClickHouse “JSON 过大”或 Node “Invalid string length” → 批次对半拆分;单条仍失败 → input/output > 1M 字符截到前 500K + [TRUNCATED: Field exceeded size limit]—services/ClickhouseWriter/jsonRecords.ts
⑥ events_core200 字符截断—0041
⑦ 导出选了 io 或 metadata 字段组才读 events_full—repositories/events.ts buildEventsForBlobStorageExportQuery
写入兜底按对象形式的 metadata 处理,不覆盖 events 表的 metadata_values 数组。元数据里的大值只能靠字段溢出处理。
建议:媒体在客户端抽走;打开字段溢出;检索结果等长文本在客户端或采集器裁剪;导出选 io / metadata 字段组;元数据只放短值。

9 脱敏

位置机制覆盖失败行为局限
① 客户端 SDKLangfuse(mask=fn),签名 fn(*, data, **kwargs);对 input/output/metadata 先抽媒体再 mask该 SDK 创建的 span抛异常 → 整段替换为 <fully masked due to failed mask function>(metadata 字典逐键)每个应用都要接;第三方埋点不经过
② OTel Collectorattributes / transform 处理器改写或删除属性经过采集器的所有 span取决于采集器配置I/O 是整段 JSON 字符串;规则语言弱
③ 服务端回调(EE)Worker 下载 S3 后 POST 整批给回调,用返回结果继续仅 OTel 路径默认放行(原始数据入库);FAIL_CLOSED=true 丢弃整批并记录 S3 key需企业许可证;旧 REST、Scores API 不经过;S3 原件未脱敏

回调契约

POST {LANGFUSE_INGESTION_MASKING_CALLBACK_URL}
Content-Type: application/json
X-Langfuse-Org-Id: <orgId>
X-Langfuse-Project-Id: <projectId>
<LANGFUSE_INGESTION_MASKING_PROPAGATED_HEADERS 中列出的上报请求头>

[ { "resource": {...}, "scopeSpans": [ { "scope": {...}, "spans": [ ... ] } ] } ]   ← 原始 resourceSpans

→ 200,响应体:同结构的 JSON(改写后的 resourceSpans)
变量默认说明
LANGFUSE_INGESTION_MASKING_CALLBACK_URL未设设置且有 EE 许可证才启用;无许可证时告警并忽略
…_CALLBACK_TIMEOUT_MS500单次超时
…_MAX_RETRIES1退避 100·2^(n−1) ms,封顶 1 s
…_CALLBACK_FAIL_CLOSEDfalsetrue:丢弃并记录 S3 key,可用 worker/src/scripts/replayIngestionEventsV2 重放
…_PROPAGATED_HEADERS空逗号分隔,大小写不敏感;Web 写进队列任务,Worker 透传给回调

packages/shared/src/server/ee/ingestionMasking · worker/src/queues/otelIngestionQueue.ts · worker/src/features/otel-ingestion/prepareOtelBatch.ts(开启 early media 时顺序:脱敏 → 校验 → 抽取媒体)

建议的三层方案

  1. 客户端:SDK mask 处理确定字段(手机号、证件号);媒体在此抽走。
  2. 网关:OTel Collector 作为唯一入口;统一加 x-langfuse-ingestion-version: 4;补租户资源属性;删除不允许出域的属性;限制批大小;采样保留根 span。
  3. 服务端:脱敏回调 + FAIL_CLOSED=true,做模型识别等复杂处理;事件桶与媒体桶 SSE-KMS + 最小权限 + 短生命周期;打开字段溢出。
  4. 导出侧:导到自有桶,先读清单,按 id 去重,用 metadata 数组还原结构;回查原件用 blob_storage_file_path。

10 双写与迁移

写入模式

legacydualevents_only(默认)
写 v3 表是是否
写 events 表否是是
NATIVE_OTEL_BEHAVIOUR=direct启动报错可用可用(默认)
NATIVE_OTEL_BEHAVIOUR=dual_write可用可用启动报错

另:events_only 要求 LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN=true。worker/src/env.ts validateV4Flags

双写传播作业

worker/src/features/eventPropagation/handleEventPropagationJob.ts · canonical/0038

历史回填(后台迁移链)

  1. createRootSpansFromTraces:span_id = t-||trace_id,type SPAN,按 yyyymm 分区分块
  2. rewriteObservationsToPidTidSorting
  3. backfillEventsFullFromObservations(JOIN traces 补属性,trace 元数据不复制)
  4. backfillEventsFullFromDatasetRunItems
  5. dropPidTidSortingTables(LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLES)
升级前 SELECT * FROM background_migrations WHERE finished_at IS NULL 必须 0 行;回填约需现有 ClickHouse 数据 3 倍磁盘;也可不回填,双写一个完整保留周期。

11 导出

项值
对象键{prefix}{projectId}/{observations_v2|scores}/{maxTimestamp}.{ext}[.gz];时间格式如 2026-11-06T10-00-00(ISO 去毫秒、冒号换连字符)
清单{prefix}{projectId}/manifests/{maxTimestamp}.json;清单出现才算完成,按 files[].key 读取
格式Parquet(默认)、CSV、JSON、JSONL;文本可 gzip
频率20 分钟、每小时、每天、每周
时间窗按 start_time [min, max);相邻窗口共享边界 → 读取方按 id 去重;查询内 LIMIT 1 BY span_id, project_id
读哪张表字段组含 io 或 metadata → events_full,否则 events_core
JSONL 直通ClickHouse JSONEachRow 直出,Worker 不逐行解析;整形写在 SQL 中
单位latency / time_to_first_token:2026-04-01 后创建的配置为秒,之前毫秒
Parquet不含 input_price / output_price / total_price;清单行数为 null;ClickHouse < 25.11 可能不完整
重试覆盖同名对象

字段组:core(必选)· basic · time · io · metadata · model · usage · prompt · metrics · trace_context · tools。packages/shared/src/domain/observation-field-groups.ts · worker/src/features/blobstorage

12 环境变量速查

变量默认作用
LANGFUSE_MIGRATION_V4_WRITE_MODEevents_onlylegacy / dual / events_only
LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOURdirectdirect / dual_write
LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_INtruev4 读路径与 v2 API
LANGFUSE_MIGRATION_V4_OTEL_DIRECT_WRITE_ORG_CREATED_CUTOFF未设Cloud 用,自部署保持未设
LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILLtrue历史回填
LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLESfalse回填后清中间表
LANGFUSE_EXPERIMENT_EVENT_PROPAGATION_PARTITION_DELAY_MINUTES10双写分区延迟
LANGFUSE_EVENT_PROPAGATION_STUCK_THRESHOLD_MINUTES35双写卡住阈值
LANGFUSE_OTEL_INGESTION_MAX_BODY_BYTES536870912OTel 请求体上限
LANGFUSE_OTEL_MEDIA_UPLOAD_ENABLEDfalse服务端媒体抽取
LANGFUSE_OTEL_EARLY_MEDIA_EXTRACTION_ENABLEDfalseRust 原生提前抽取
LANGFUSE_OBSERVATION_FIELD_OVERFLOW_ENABLEDfalse字段溢出到媒体桶
LANGFUSE_OBSERVATION_FIELD_SIZE_LIMIT_BYTES2097152溢出阈值
LANGFUSE_S3_MEDIA_MAX_CONTENT_LENGTH1000000000单个媒体文件上限
LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE / _INTERVAL_MS / _MAX_ATTEMPTS1000 / 1000 / 3写入缓冲
LANGFUSE_INGESTION_MASKING_*见 §9脱敏回调
LANGFUSE_S3_EVENT_UPLOAD_SSE / _SSE_KMS_KEY_ID未设事件桶加密

13 文档与代码的差异

项文档代码(6918181)
“实时写入”SDK 版本Python ≥ 4.7.0、JS ≥ 5.4.0服务端直写:Python ≥ 4.0.0、JS ≥ 5.0.0(checkHeaderBasedDirectWrite);4.7 / 5.4 是 SDK 复制 trace 属性的版本
双写卡住阈值15 分钟35 分钟
双写延迟约 15 分钟 / 博客“3 分钟 + 5 分钟”3 分钟分区 + 10 分钟延迟
JSON 类型25.12 用于 JSON 类型events 表 metadata 为两个 Array(String),未用 JSON 列

14 改造风险清单

15 代码位置索引

主题文件
OTel 入口web/src/pages/api/public/otel/v1/traces/index.ts · web/src/server/otel/processOtelIngestion.ts · otelRequestBody.ts
旧 REST 入口web/src/pages/api/public/ingestion.ts · packages/shared/src/server/ingestion/processEventBatch.ts · eventBucketPath.ts
span → 事件packages/shared/src/server/otel/OtelIngestionProcessor.ts · otel/utils.ts
Worker OTel 队列 / 写入路径worker/src/queues/otelIngestionQueue.ts · worker/src/features/otel-ingestion/
补全与写入worker/src/services/IngestionService/index.ts · worker/src/services/ClickhouseWriter/
字段溢出worker/src/features/observation-field-overflow/processObservationFieldOverflow.ts
媒体web/src/pages/api/public/media/ · packages/shared/src/server/media/mediaService.ts · otel/OtelMediaProcessor.ts
脱敏回调packages/shared/src/server/ee/ingestionMasking/
ClickHouse 迁移packages/shared/clickhouse/migrations/canonical/0038–0049
读路由packages/shared/src/server/query-ast/eventsRouting.ts · repositories/events.ts
双写传播worker/src/features/eventPropagation/handleEventPropagationJob.ts
回填worker/src/backgroundMigrations/
导出worker/src/features/blobstorage/ · packages/shared/src/domain/observation-field-groups.ts
环境变量worker/src/env.ts · web/src/env.mjs · packages/shared/src/env.ts
Python SDKlangfuse/_client/span_processor.py · attributes.py · propagation.py · span.py · _task_manager/media_manager.py · media.py
本页由 AI 根据 Langfuse 源码与官方文档整理(2026-11)。官方资料:v4 changelog · v3→v4 升级指南 · 自定义上报迁移 · 导出到对象存储 · Simplifying Langfuse for Scale · 源码 langfuse/langfuse · langfuse-python