Langfuse 当前 repo 完全导读
基于当前工作区编写:
/Users/zhangxian/projects/langfuse。 本教程的目标不是罗列文件,而是教你如何拆解这个 repo:先建立静态地图,再追运行链路,最后抓住契约和抽象。
目录
- 0. 学习路线:如何读一个大型 repo
- 1. 项目定位:Langfuse 在解决什么问题
- 2. 静态地图:monorepo 的组织方式
- 3. 五层架构:从入口到数据设施
- 4. 运行原理:五条主链路
- 5. 契约中心:为什么 shared 是关键
- 6. 数据层:Postgres、ClickHouse、Redis、S3 怎么分工
- 7. 前端与颜色系统:UI 是怎样组织的
- 8. 开发路线:新增功能时该从哪里下手
- 9. 读代码练习:从任务倒推源码
- 10. 术语表与常见误区
0. 学习路线:如何读一个大型 repo
0.1 本教程的拆解方法
读 Langfuse 这样的 repo,最容易犯的错误是从目录树第一行开始随机点文件。正确方法是把 repo 当成一个正在运行的系统看:
- 先问入口:请求、任务、事件从哪里进入系统?
- 再看边界:哪个 package 拥有这个入口?它能不能直接访问数据?
- 找契约:这个流程依赖哪些 Zod schema、TypeScript type、队列 payload、查询定义?
- 追运行链路:同步返回,还是进入 Redis/BullMQ 后由 worker 异步处理?
- 落到数据层:最终写 Postgres、ClickHouse、Redis、S3,还是调用外部系统?
这五个问题比“这个文件叫什么”更重要。文件名会变,边界和契约的方向更稳定。
0.2 读 repo 的两只眼睛
| 视角 | 回答的问题 | 在 Langfuse 里看什么 |
|---|---|---|
| 静态地图 | 代码住在哪,谁能依赖谁 | pnpm-workspace.yaml、package.json、web/、worker/、packages/shared/ |
| 动态链路 | 运行时谁先调用谁 | tRPC、Public API、ingestion、BullMQ、ClickHouseWriter |
| 契约地图 | 哪些规则不能随便改 | Zod schema、FilterState、queue payload、domain model、query builder |
| 数据地图 | 为什么有多种存储 | Postgres、ClickHouse、Redis、S3 的职责分工 |
| 修改地图 | 改一个功能会波及哪里 | web 入口、shared 契约、worker 消费者、Fern/API、测试 |
0.3 本教程使用的核心源码锚点
| 主题 | 入口文件 |
|---|---|
| monorepo 包结构 | pnpm-workspace.yaml、package.json、turbo.json |
| 架构原则 | .agents/ARCHITECTURE_PRINCIPLES.md |
| tRPC 根路由 | web/src/server/api/root.ts |
| tRPC procedure 和鉴权 | web/src/server/api/trpc.ts |
| Public API 包装器 | web/src/features/public-api/server/createAuthedProjectAPIRoute.ts、withMiddlewares.ts |
| 队列契约 | packages/shared/src/server/queues.ts |
| ingestion 入队 | packages/shared/src/server/ingestion/processEventBatch.ts |
| worker 注册 | worker/src/app.ts |
| ingestion worker | worker/src/queues/ingestionQueue.ts |
| ClickHouse 批量写入 | worker/src/services/ClickhouseWriter/index.ts |
| v4 events 表结构 | packages/shared/clickhouse/scripts/dev-tables.sh |
| 筛选契约 | packages/shared/src/interfaces/filters.ts、web/src/features/search-bar/README.md |
| UI token 和颜色 | web/src/styles/globals.css |
1. 项目定位:Langfuse 在解决什么问题
1.1 一句话理解
Langfuse 是一个开源 LLM engineering 平台,用来开发、监控、评估和调试 AI 应用。代码层面看,它不是一个单纯的 Next.js 应用,而是一个围绕高吞吐 telemetry、trace、observation、score、dataset、prompt、eval 的多入口数据平台。
1.2 教学类比:机场运行系统
可以把 Langfuse 想成一个机场运行系统:
| 机场类比 | Langfuse 对应物 | 解释 |
|---|---|---|
| 旅客和航班数据不断进入 | SDK、Public API、OTel ingestion | 外部应用持续上报 trace、span、score、dataset 结果 |
| 值机柜台和安检 | web/ API 入口 | 鉴权、校验、限流、上下文设置 |
| 调度中心 | packages/shared 契约和服务 | 定义所有入口和 worker 都必须遵守的规则 |
| 后台行李系统 | worker/ | 大批量、可重试、耗时工作离开请求链路 |
| 航班历史数据库 | ClickHouse | 存高吞吐、宽表、可分析事件 |
| 会员和订单系统 | PostgreSQL | 存组织、项目、配置、权限、事务型数据 |
这个类比的关键是:前台入口不能自己发明规则,后台系统不能猜 payload,所有关键规则必须沉到 shared 契约里。
1.3 当前架构原则
项目自己的架构原则强调高规模、探索式 observability 和宽事件模型。最重要的几条可以转成工程判断:
- observation/event 是主要分析单位,trace 更像相关性句柄。
- 高基数上下文应该被保留,方便用户后续任意切分和过滤。
- 列式存储和查询路径要围绕时间窗口、字段选择、排序键、数据裁剪设计。
- 列表和聚合视图应该读轻量投影,大字段只在详情页或必要场景读取。
- 公共 API 要规模感知:时间窗口、字段选择、分页和避免全历史扫描。
这解释了为什么 repo 里同时存在 events_full 和 events_core,也解释了为什么查询构建器、FilterState、列映射这些契约非常重要。
2. 静态地图:monorepo 的组织方式
2.1 顶层目录
langfuse/
├── web/ # Next.js app:UI、tRPC、Public REST API、ingestion HTTP 入口
├── worker/ # Queue consumer 和后台处理
├── packages/shared/ # 共享领域模型、DB、队列契约、仓储、服务
├── ee/ # 企业版能力,被 web 引用
├── generated/ # 生成的 API client,不手改
├── fern/ # Public API 定义源
├── scripts/ # repo 脚本
└── docs/ # 本地补充文档和架构图2.2 package 依赖方向
Langfuse 的关键边界是依赖方向:
读图规则:
web可以依赖shared和ee。worker只能依赖shared,不能反向依赖web。ee可以依赖shared。shared不能依赖web、worker、ee。
这就是 repo 的“重力方向”:越底层越稳定,越上层越接近具体入口。
2.3 monorepo 工具链
| 文件 | 作用 |
|---|---|
package.json | 根脚本、Node 版本、pnpm 版本、Turbo 命令 |
pnpm-workspace.yaml | workspace 包:web、worker、packages/**、ee |
turbo.json | build、dev、lint、typecheck、test 的任务依赖和缓存策略 |
当前根 package 约束:
- Node:
24 - pnpm:
11.4.0 - 根命令通过 Turbo 分发到各 package
db:generate不缓存,因为 Prisma generate 写入node_modules,不能只依赖日志回放
2.4 不同目录的阅读顺序
| 你想理解 | 先读 | 再读 |
|---|---|---|
| UI 如何调用后端 | web/src/server/api/root.ts | 具体 router 和 React 调用处 |
| SDK/Public API 怎么进来 | web/src/pages/api/public/** | features/public-api/server/** 和 features/public-api/types/** |
| trace/observation 怎么写入 | processEventBatch.ts | worker/src/queues/ingestionQueue.ts、IngestionService、ClickhouseWriter |
| 后台任务怎么跑 | worker/src/app.ts | worker/src/queues/**、packages/shared/src/server/queues.ts |
| 筛选和表格怎么连到 ClickHouse | search-bar/README.md | filters.ts、event-query-builder.ts |
| 数据模型怎么定义 | Prisma schema、ClickHouse migrations | shared domain/repository definitions |
3. 五层架构:从入口到数据设施
3.1 架构图
draw.io XML 图已经放在:
architecture/langfuse-current-architecture.drawio
图中分成五层:
- 外部入口
web/同步产品界面与 API 入口packages/shared契约、领域模型、服务、仓储worker/异步执行与后台处理- 数据与基础设施
3.2 五层职责
| 层 | 主要模块 | 职责 |
|---|---|---|
| 外部入口 | Browser、SDK、Public API client、OTel、内部触发器 | 发起 UI 查询、REST 请求、telemetry ingestion 或后台任务 |
web/ | Next.js UI、tRPC、Public REST API、ingestion API | 同步请求入口,负责鉴权、校验、限流、上下文 |
packages/shared | domain、filter、query、queue、repository、service | 契约中心,定义 web 和 worker 共同遵守的规则 |
worker/ | WorkerManager、queue processor、IngestionService、ClickhouseWriter | 异步处理耗时或可重试任务 |
| 数据与基础设施 | PostgreSQL、ClickHouse、Redis、S3、外部系统 | 持久化、队列、缓存、对象存储、外部副作用 |
3.3 为什么 shared 是第三层,而不是工具包
packages/shared 不是普通 utils 包。它承载的是系统契约:
- domain schema:trace、observation、score、event 的运行时校验和 TS 类型
- queue schema:BullMQ job 的名字、payload、类型映射
- query contract:字段、列映射、ClickHouse 查询构建
- filter contract:前端筛选、搜索栏语法、后端 SQL lowering 的共同语言
- repository:复杂数据读取和转换
- service:跨 web/worker 复用的后端能力
换句话说,shared 是“系统宪法”,不是“杂物间”。
3.4 颜色和图例怎么读
架构图颜色不是装饰,它对应边界:
| 颜色 | 含义 |
|---|---|
| 灰色 | 外部入口和外部系统 |
| 靛蓝 | web/ 同步入口 |
| 琥珀 | shared 契约中心 |
| 绿色 | worker 异步处理 |
| 红色 | 数据与基础设施 |
实线表示主运行链路。虚线表示条件路径、legacy 路径或双写路径。
4. 运行原理:五条主链路
4.1 链路一:UI 通过 tRPC 读写数据
用户在浏览器中操作 UI,通常走 tRPC。
源码锚点:
appRouter在web/src/server/api/root.ts聚合所有 router。createTRPCRouter、authenticatedProcedure、protectedProjectProcedure在web/src/server/api/trpc.ts。protectedProjectProcedure会要求输入包含projectId,并校验用户是否属于该项目。
读代码时要看三件事:
- 这个 procedure 是 query 还是 mutation?
- 输入 schema 是什么,是否包含
projectId? - 业务逻辑是否委托给 service/repository,而不是塞在 router 里?
4.2 链路二:Public REST API 供 SDK 和外部系统调用
SDK 或外部系统调用 /api/public/**,通常走 Next.js API route。
源码锚点:
web/src/features/public-api/server/withMiddlewares.ts:CORS、method、统一错误处理、ClickHouse resource error 处理。web/src/features/public-api/server/createAuthedProjectAPIRoute.ts:API key scope、admin API key、自托管限制、rate limit、Zod request/response schema。web/src/features/public-api/README.md:新增 Public API 的流程。
和 tRPC 的区别:
| 对比项 | tRPC | Public REST API |
|---|---|---|
| 使用者 | Langfuse Web UI | SDK、外部集成、用户脚本 |
| 鉴权 | NextAuth session + project membership | API key Basic/Bearer + access level |
| 类型来源 | tRPC + Zod 输入 | Public API types + Zod request/response |
| 对外稳定性 | 内部 API | 公共契约,需要同步 Fern 和 SDK |
4.3 链路三:ingestion 从请求变成异步写入
这是 Langfuse 最核心的数据链路。外部 SDK 上报 trace/observation/score 时,不应该让 HTTP 请求一直等 ClickHouse 合并和写入完成。因此 ingestion 采用“先收下、再异步处理”的模型。
源码锚点:
packages/shared/src/server/ingestion/processEventBatch.ts:请求侧批量处理、事件校验、S3 上传、入队。packages/shared/src/server/queues.ts:IngestionEvent、QueueName.IngestionQueue、QueueJobs.IngestionJob等队列契约。worker/src/queues/ingestionQueue.ts:worker 消费 job,读取 S3,处理 secondary queue,调用 IngestionService。worker/src/services/IngestionService/index.ts:把松散事件转换为严格 ClickHouse record。worker/src/services/ClickhouseWriter/index.ts:按表维护内存队列,批量刷写 ClickHouse。
关键直觉:
- HTTP 请求只完成“接收、校验、排队”,重活交给 worker。
- S3 存原始事件正文,Redis 只放小 payload 和指针。
- queue payload 由 shared 定义,producer 和 consumer 不允许各自猜字段。
- ClickhouseWriter 是批量写入缓冲层,避免每个事件单独打 ClickHouse。
4.4 链路四:worker 不是一个任务,而是一组可开关消费者
worker/src/app.ts 是 worker 侧的总装配入口。它不是只启动一个 consumer,而是根据 env 开关注册很多队列:
- trace upsert/delete
- ingestion / secondary ingestion
- OTel ingestion / secondary OTel ingestion
- eval execution / LLM-as-judge / code eval
- batch export
- batch action
- project/dataset/score delete
- monitor、notification、webhook、integration 等
读 worker 代码时,先从 WorkerManager.register(queueName, processor, options) 看:
- 这个 queue 是否有 env 开关?
- concurrency、limiter、lockDuration、maxStalledCount 怎么设?
- processor 的输入 payload 是否来自
packages/shared/src/server/queues.ts? - processor 成功和失败分别代表什么?失败是否应该重试?
4.5 链路五:v4 events 查询从 FilterState 变成 ClickHouse SQL
v4 events 表把 observation/trace 相关属性 denormalize 到宽事件中。前端筛选、搜索栏、表格列和后端查询需要共享同一套语言。
源码锚点:
web/src/features/search-bar/README.md:说明 URL filter state 是单一事实源。packages/shared/src/interfaces/filters.ts:singleFilter、eventsTableFilterState、filter operator。packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts:字段映射、field set、自动project_idfilter、排序优化、trace_id hash 优化。
这条链路的关键不是 UI,而是契约一致性:搜索栏只是 FilterState 的编辑器,不能绕过 filter contract 自己发明一种后端不懂的查询语言。
5. 契约中心:为什么 shared 是关键
5.1 契约的类型
Langfuse 里常见的契约不是单一接口,而是一组组合规则:
| 契约 | 文件例子 | 解决的问题 |
|---|---|---|
| domain schema | packages/shared/src/domain/**、repositories/definitions.ts | 运行时数据长什么样 |
| queue payload | packages/shared/src/server/queues.ts | web 入队和 worker 消费必须一致 |
| filter state | packages/shared/src/interfaces/filters.ts | UI 筛选和后端查询共享语言 |
| query builder | packages/shared/src/server/queries/** | 避免散落手写 SQL |
| repository | packages/shared/src/server/repositories/** | 复杂数据读取、转换、tenant filter |
| public API type | web/src/features/public-api/types/** | 外部 API 的请求/响应稳定性 |
| UI table contract | web/src/components/table/** | 列定义、排序、可见性、分页、行选择 |
5.2 Zod + TypeScript 的模式
很多数据结构会同时具备:
- Zod schema,负责运行时校验。
z.infer<>type,负责 TypeScript 静态类型。- 转换函数,负责 DB record 和 domain model 之间的映射。
例子:
observationRecordReadSchematraceRecordInsertSchemascoreRecordInsertSchemaeventRecordBaseSchemaeventRecordInsertSchema
这种模式的价值是:数据从外部进入系统时能被校验,进入代码后又有 TS 类型跟踪。
5.3 queue contract 是 web 和 worker 的共同边界
packages/shared/src/server/queues.ts 里有三个层次:
- payload schema,例如
IngestionEvent、OtelIngestionEvent、EvalExecutionEvent。 - queue name,例如
QueueName.IngestionQueue。 - job name 和类型映射,例如
QueueJobs.IngestionJob、TQueueJobTypes。
为什么这么设计?
- producer 可以类型安全地 add job。
- consumer 可以类型安全地读 job.data。
- rolling deploy 时可以通过 optional 字段兼容旧 job。
- 队列名和 payload 不会在 web/worker 两边漂移。
如果你要新增 worker job,先改 queues.ts,再写 processor,最后注册到 worker/src/app.ts。
5.4 FilterState 是筛选系统的中心
FilterState 是扁平数组,元素是 discriminated union:
- datetime
- string
- number
- stringOptions
- categoryOptions
- arrayOptions
- stringObject
- numberObject
- boolean
- null
- positionInTrace
搜索栏 grammar 不能表达的形状,要么保留为 skipped filter,要么报 commit-blocking diagnostic。不能静默丢弃。
这体现了一个大型 UI 契约的原则:新的交互方式要编辑已有事实源,而不是另造一份状态。
5.5 查询构建器的职责
event-query-builder.ts 做了几件关键事:
- 统一字段映射:例如
traceId映射到e.trace_id as "trace_id"。 - 定义 field set:列表查询、详情查询、metadata、tools、I/O 等按需选择。
- 自动加
project_idfilter:避免租户数据串读。 - 对 events 表排序做优化:按
project_id、toStartOfMinute(start_time)等贴合主键。 - 对 trace_id equality 加
xxHash32(trace_id)优化。 - 默认读
events_core,需要完整 I/O 时再读events_full。
读查询代码时,不要只看 SQL 字符串,要看 builder 在保护什么约束。
6. 数据层:Postgres、ClickHouse、Redis、S3 怎么分工
6.1 总览
| 存储 | 用途 | 典型数据 |
|---|---|---|
| PostgreSQL + Prisma | 事务型、关系型、配置型数据 | org、project、user、API key、dataset、prompt、eval config |
| ClickHouse | 高吞吐、宽表、分析查询 | traces、observations、scores、events_full、events_core |
| Redis + BullMQ | 队列、限流、缓存、锁 | ingestion job、eval job、rate limit、recently processed cache |
| S3 / Blob storage | 大对象和原始事件正文 | raw ingestion event、media、export 文件 |
| 外部系统 | 副作用和集成 | LLM providers、webhook targets、Slack、Stripe、PostHog |
6.2 为什么不能只用一个数据库
PostgreSQL 擅长事务和关系。ClickHouse 擅长大规模分析和列式扫描。Langfuse 同时需要:
- 管组织、项目、权限、配置、API key。
- 高速写入大量 observation/event。
- 按时间、metadata、score、model、usage/cost 做探索式查询。
- 在列表页尽快返回轻量数据,在详情页再取完整 payload。
一个数据库很难同时把这些场景都做好,所以 repo 里清晰区分:
- 配置和事务:Postgres。
- 遥测分析:ClickHouse。
- 异步任务和限流:Redis。
- 大 payload 和原始日志:S3。
6.3 ClickHouse legacy 表和 v4 events
当前代码里同时存在 legacy 表和 v4 events 路径:
| 表 | 角色 |
|---|---|
traces、observations、scores | legacy ClickHouse 表 |
observations_batch_staging | 批量 staging,带 TTL |
events_full | v4 全量保真事件表,包含完整 input/output/metadata |
events_core | v4 轻量查询投影,面向列表和常用查询 |
events_full 的设计重点:
- 宽事件,一行携带 trace、span、model、usage、cost、metadata、tool、experiment 等上下文。
- 使用
ReplacingMergeTree(event_ts, is_deleted)。 - 主键和排序围绕
project_id、toStartOfMinute(start_time)、xxHash32(trace_id)。 - 对 input/output/metadata 建索引和压缩。
events_core 的设计重点:
- 从
events_full生成轻量投影。 - 列表和常用查询默认读它,避免扫描大字段。
- 详情或完整导出才需要读 full fidelity 数据。
6.4 ClickhouseWriter 的缓冲模型
ClickhouseWriter 是 worker 内部的单例写入器:
它处理的不只是“写入”:
- 批量。
- 定时 flush。
- 网络重试。
- 字符串长度错误时拆 batch。
- 记录过大时截断。
- Decimal64 overflow clamp。
- 指标和日志。
所以 ingestion processor 不应该自己直接 insert ClickHouse,而是把 record 交给 writer。
6.5 数据隔离的硬规则
几乎所有项目级数据访问都必须带 projectId / project_id。
- Prisma 查询要按
projectId过滤。 - ClickHouse 查询要按
project_id过滤。 - query builder 会自动加 project filter,但手写查询时必须主动检查。
- Public API auth scope 中必须拿到 projectId。
- tRPC
protectedProjectProcedure会把 projectId 和 session membership 绑定起来。
这是 multi-tenant 系统的底线。
7. 前端与颜色系统:UI 是怎样组织的
7.1 前端技术栈
web/ 是 Next.js 应用,主要栈包括:
- Next.js
- React
- tRPC
- TanStack Query
- TanStack Table
- Tailwind CSS v4
- Radix UI
- Zustand
- Recharts
- Storybook / Vitest / Playwright
但理解前端时不要从依赖列表开始,应该从页面工作流和 feature 目录开始。
7.2 feature 组织方式
web/src/features/** 下放很多业务模块,例如:
- datasets
- prompts
- evals
- public-api
- search-bar
- tracing-tables
- score-analytics
- blobstorage-integration
- slack
- telemetry
- mcp
典型 feature 会包含:
feature/
├── components/ # React 组件
├── server/ # router、service、server action
├── types/ # 类型和 schema
├── hooks/ # 客户端 hook
└── README.md # 复杂 feature 的本地契约说明不是所有 feature 都完全一致,但大方向是“业务相关代码尽量聚在一起”。
7.3 表格抽象
Langfuse 是 observability 产品,表格是核心 UI。DataTable 封装了很多通用能力:
- column visibility
- column order
- column sizing
- sorting
- pagination
- row selection
- pinned columns
- row height
- peek view
- loading/error/empty state
源码锚点:
web/src/components/table/data-table.tsxweb/src/components/table/typesweb/src/features/tracing-tables/README.md
读表格代码时,要区分:
- 表格容器负责数据请求和 URL/filter 状态。
- column definition 负责列展示和排序语义。
DataTable负责通用交互和布局。
7.4 颜色系统
颜色 token 定义在 web/src/styles/globals.css。
整体特点:
- 默认浅色:白色背景、中性文字、浅灰边框。
- 深色:深蓝灰背景、中等亮度前景色。
- 主强调色:靛蓝/紫蓝系,例如
--primary-accent。 - 状态色:红、黄、绿、蓝都有浅色和深色 token。
- 图表色:多色序列,不是单一品牌色渐变。
- Search bar 语法高亮有独立 token:field、value、number、keyword。
- 圆角基础值是
0.5rem,偏实用工具界面。
可以把 UI 颜色理解成三层:
这和产品定位一致:Langfuse 是高密度分析工具,不是营销页。颜色服务于扫描、对比、状态识别和数据可视化。
7.5 Search Bar 是一个契约型 UI
Search Bar 的设计特别值得学,因为它展示了大型前端功能怎样不破坏已有系统:
- 它不是新的筛选状态源。
- URL filter state 仍然是事实源。
- bar 只是受控编辑器。
- facet sidebar 也是同一事实源的另一个编辑器。
- commit 前必须 validate,并 lower 到
FilterState。 - 不能表达的筛选不能静默丢弃。
这类 UI 的核心不是 contenteditable,而是“状态所有权”。
8. 开发路线:新增功能时该从哪里下手
8.1 新增一个 UI 内部功能
适合场景:只给 Langfuse Web UI 使用,不作为 Public API 暴露。
路线:
- 在对应
web/src/features/[feature]或web/src/server/api/routers找现有 router。 - 定义 Zod input schema。
- 使用
protectedProjectProcedure或其他合适 procedure。 - 把业务逻辑放到 service/repository,不要写满 router。
- 需要复杂 ClickHouse 读写时优先放 shared repository。
- 前端通过
api.[router].[procedure].useQuery/useMutation调用。 - 写 targeted web test。
自检:
- 输入里是否有
projectId? - 是否校验 project membership?
- 是否复用 shared 契约?
- 是否把 business logic 塞进 route 了?
8.2 新增 Public API endpoint
适合场景:SDK、外部用户、自动化脚本需要稳定调用。
路线:
- 在
web/src/pages/api/public/**建 route。 - 使用
withMiddlewares包住 HTTP method。 - 使用
createAuthedProjectAPIRoute做鉴权、限流、Zod 校验。 - request/response schema 放到
web/src/features/public-api/types/**。 - 响应也要被 Zod 校验,避免返回未声明字段。
- 更新 Fern sources。
- 更新 generated client 或相关 SDK 类型。
- 写 server API 测试。
自检:
- 这是不是公共契约?如果是,不能只改 web route。
- response schema 是否 strict?
- 错误是否使用 shared error 类型?
- Fern/API docs 是否同步?
8.3 新增 worker 队列任务
适合场景:耗时、可重试、批量、调用外部系统或不应阻塞 HTTP 请求的工作。
路线:
- 在
packages/shared/src/server/queues.ts定义 payload schema。 - 添加
QueueName、QueueJobs、TQueueJobTypes映射。 - 在合适 producer 里 add job。
- 在
worker/src/queues/**写 processor。 - 在
worker/src/app.ts用WorkerManager.register注册。 - 设置 concurrency、limiter、lockDuration、maxStalledCount。
- 写 worker vitest。
自检:
- payload 是否可以 rolling deploy 兼容?
- processor 失败是否真的应该 retry?
- 是否需要 dead letter 或 recorded error?
- 是否有 tenant/project filter?
8.4 修改 ingestion 或 v4 event 字段
这是高风险路径,因为会影响 SDK ingestion、worker、ClickHouse、UI 查询和可能的 API。
路线:
- 先看
packages/shared/src/server/ingestion/types和 domain/repository definitions。 - 修改 event record schema。
- 修改 ClickHouse migration 或 dev table。
- 修改 IngestionService 转换逻辑。
- 修改 ClickhouseWriter TableName 或 record type,如果新增表。
- 修改 query builder field mapping。
- 修改 UI column/filter/search registry。
- 增加 ingestion worker 测试和查询测试。
自检:
- 字段是 full fidelity 还是 core projection 也需要?
- 是否应该 denormalize?
- 是否会扫描大字段?
- 是否需要 migration/backfill?
- legacy 和 v4 双写模式下行为是否一致?
8.5 修改 Search Bar 或筛选语法
必须先读 web/src/features/search-bar/README.md。
路线:
- 明确
FilterState是否能表达新语义。 - 如果不能表达,不要只改 parser。
- 同步更新 field registry、validate、adapter、reverse adapter、commit gate。
- 保持 validate 和 lower parity。
- 增加 property/invariant 测试。
- 检查 URL round-trip。
自检:
- 是否会静默丢 filter?
- sidebar 和 search bar 是否仍读写同一事实源?
- negation 是否能 lower 到现有 inverse operator?
- free text search 的 scope 是否仍一致?
9. 读代码练习:从任务倒推源码
9.1 练习一:追踪一次 UI trace 列表查询
目标:弄清楚 trace list 如何从 UI 变成 ClickHouse 查询。
步骤:
- 从 trace 页面或 tracing table 组件开始找
api.traces或 events table 调用。 - 找到 tRPC router:
web/src/server/api/root.ts里traces: traceRouter。 - 找到具体 procedure。
- 看 input schema 是否包含
projectId、filter、orderBy、pagination。 - 找到 shared repository 或 query builder。
- 确认 ClickHouse 查询是否带
project_id和时间范围。
你要能回答:
- 这个列表读 legacy 表还是 v4 events?
- 哪些字段是列表页需要的?
- input/output 是否被延迟读取?
9.2 练习二:追踪一次 SDK ingestion
目标:从 HTTP 请求追到 ClickHouse 写入。
步骤:
- 找 ingestion API route。
- 找它如何调用
processEventBatch。 - 在
processEventBatch中看校验、分组、S3 上传和入队。 - 在
queues.ts中看 payload 契约。 - 在
worker/src/app.ts中看 ingestion queue 是否注册。 - 在
worker/src/queues/ingestionQueue.ts中看 S3 读取、secondary queue、mergeAndWrite。 - 在
IngestionService中看事件如何转为 record。 - 在
ClickhouseWriter中看批量写入。
你要能回答:
- 为什么 Redis job 不直接塞完整 event body?
- S3 SlowDown 时为什么有 secondary queue?
- 为什么 worker 要从 event body 取 canonical entity id?
9.3 练习三:新增一个 Public API 字段
目标:理解公共契约的修改面。
步骤:
- 找对应
web/src/pages/api/public/**route。 - 找 response schema。
- 找 service/repository。
- 找 Fern API 定义。
- 找 generated client 或 SDK 类型影响。
- 写 API 测试校验 response schema。
你要能回答:
- 这个字段来自 Postgres 还是 ClickHouse?
- 是否会触发大字段读取?
- 旧客户端看到这个字段是否安全?
9.4 练习四:判断一个变化应该放哪层
| 变化 | 应该先看 |
|---|---|
| 新增一个项目设置项 | Postgres schema、settings UI、tRPC router |
| 新增一种后台清理任务 | queues.ts、worker/src/app.ts、worker processor |
| 新增 observation 过滤字段 | FilterState、field registry、query builder、ClickHouse schema |
| 新增表格列 | table column definition、query field set、repository |
| 新增 SDK endpoint | Public API route、types、Fern、SDK |
| 新增 ingestion event 字段 | ingestion types、IngestionService、ClickHouse schema、query builder |
10. 术语表与常见误区
10.1 术语表
| 术语 | 在本 repo 中的意思 |
|---|---|
web | Next.js app,包含 UI、tRPC、Public API、ingestion HTTP 入口 |
worker | BullMQ consumer 和后台任务进程 |
shared | web/worker/ee 共享的契约、领域、仓储、服务 |
| tRPC | Web UI 内部类型安全 API |
| Public API | 面向 SDK 和外部用户的 REST API |
protectedProjectProcedure | tRPC 的项目级 session/RBAC 中间件 |
createAuthedProjectAPIRoute | Public API 的 API key 鉴权和 Zod 校验包装器 |
FilterState | 表格筛选的统一状态契约 |
events_full | v4 full fidelity ClickHouse event 表 |
events_core | v4 query-optimized event 投影 |
ClickhouseWriter | worker 内部批量写 ClickHouse 的单例 |
WorkerManager | worker 队列注册和管理入口 |
Fern | Public API 定义和生成物来源 |
10.2 常见误区
误区一:看到 Next.js 就以为所有后端逻辑都在 web。 实际:web 是入口层。很多稳定业务契约和数据访问在 shared,耗时工作在 worker。
误区二:把 shared 当 utils 包。 实际:shared 是契约中心。随便改 shared 会影响 web、worker、ee。
误区三:Public API 和 tRPC 可以复用同一套随意返回对象。 实际:Public API 是外部契约,需要 Zod response schema、Fern 和 SDK 同步。
误区四:ingestion 是同步写 ClickHouse。 实际:请求侧上传 S3 并入队,worker 异步读取、合并、批量写。
误区五:Search Bar 有自己的筛选状态。 实际:URL FilterState 是事实源,Search Bar 和 sidebar 都是编辑器。
误区六:ClickHouse 查询只要能跑就行。 实际:必须考虑 project_id、时间窗口、排序键、字段选择、大字段延迟读取、query builder 复用。
10.3 最小上手命令
pnpm install
pnpm run infra:dev:up
pnpm run db:generate
pnpm run dev:web
pnpm run dev:worker常用验证:
pnpm run lint
pnpm run typecheck
pnpm --filter web run test
pnpm --filter worker run test
pnpm --filter @langfuse/shared run test种子数据:
pnpm run seed -- list涉及 UI 的改动,优先使用 seed CLI 生成场景,然后在浏览器里检查真实页面。
最后总结
读 Langfuse 的核心路线可以压缩成一句话:
外部请求进入 web,稳定规则沉到 shared,耗时任务交给 worker,事务数据进 Postgres,分析事件进 ClickHouse,大 payload 放 S3,队列和限流交给 Redis。
进一步拆开,就是四个判断:
- 这是同步产品/API 请求,还是异步后台任务?
- 这是内部 UI 契约,还是外部 Public API 契约?
- 这个数据是事务配置,还是高吞吐分析事件?
- 这个规则应该属于入口层,还是应该沉到 shared 供 web/worker 共同使用?
只要这四个判断清楚,修改 Langfuse 的大多数路径都能落到正确位置。