Skip to content

Langfuse 当前 repo 完全导读

基于当前工作区编写:/Users/zhangxian/projects/langfuse。 本教程的目标不是罗列文件,而是教你如何拆解这个 repo:先建立静态地图,再追运行链路,最后抓住契约和抽象。

目录


0. 学习路线:如何读一个大型 repo

0.1 本教程的拆解方法

读 Langfuse 这样的 repo,最容易犯的错误是从目录树第一行开始随机点文件。正确方法是把 repo 当成一个正在运行的系统看:

  1. 先问入口:请求、任务、事件从哪里进入系统?
  2. 再看边界:哪个 package 拥有这个入口?它能不能直接访问数据?
  3. 找契约:这个流程依赖哪些 Zod schema、TypeScript type、队列 payload、查询定义?
  4. 追运行链路:同步返回,还是进入 Redis/BullMQ 后由 worker 异步处理?
  5. 落到数据层:最终写 Postgres、ClickHouse、Redis、S3,还是调用外部系统?

这五个问题比“这个文件叫什么”更重要。文件名会变,边界和契约的方向更稳定。

0.2 读 repo 的两只眼睛

视角回答的问题在 Langfuse 里看什么
静态地图代码住在哪,谁能依赖谁pnpm-workspace.yamlpackage.jsonweb/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.yamlpackage.jsonturbo.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.tswithMiddlewares.ts
队列契约packages/shared/src/server/queues.ts
ingestion 入队packages/shared/src/server/ingestion/processEventBatch.ts
worker 注册worker/src/app.ts
ingestion workerworker/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.tsweb/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_fullevents_core,也解释了为什么查询构建器、FilterState、列映射这些契约非常重要。


2. 静态地图:monorepo 的组织方式

2.1 顶层目录

text
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 可以依赖 sharedee
  • worker 只能依赖 shared,不能反向依赖 web
  • ee 可以依赖 shared
  • shared 不能依赖 webworkeree

这就是 repo 的“重力方向”:越底层越稳定,越上层越接近具体入口。

2.3 monorepo 工具链

文件作用
package.json根脚本、Node 版本、pnpm 版本、Turbo 命令
pnpm-workspace.yamlworkspace 包:webworkerpackages/**ee
turbo.jsonbuild、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.tsworker/src/queues/ingestionQueue.tsIngestionServiceClickhouseWriter
后台任务怎么跑worker/src/app.tsworker/src/queues/**packages/shared/src/server/queues.ts
筛选和表格怎么连到 ClickHousesearch-bar/README.mdfilters.tsevent-query-builder.ts
数据模型怎么定义Prisma schema、ClickHouse migrationsshared domain/repository definitions

3. 五层架构:从入口到数据设施

3.1 架构图

draw.io XML 图已经放在:

architecture/langfuse-current-architecture.drawio

图中分成五层:

  1. 外部入口
  2. web/ 同步产品界面与 API 入口
  3. packages/shared 契约、领域模型、服务、仓储
  4. worker/ 异步执行与后台处理
  5. 数据与基础设施

3.2 五层职责

主要模块职责
外部入口Browser、SDK、Public API client、OTel、内部触发器发起 UI 查询、REST 请求、telemetry ingestion 或后台任务
web/Next.js UI、tRPC、Public REST API、ingestion API同步请求入口,负责鉴权、校验、限流、上下文
packages/shareddomain、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。

源码锚点:

  • appRouterweb/src/server/api/root.ts 聚合所有 router。
  • createTRPCRouterauthenticatedProcedureprotectedProjectProcedureweb/src/server/api/trpc.ts
  • protectedProjectProcedure 会要求输入包含 projectId,并校验用户是否属于该项目。

读代码时要看三件事:

  1. 这个 procedure 是 query 还是 mutation?
  2. 输入 schema 是什么,是否包含 projectId
  3. 业务逻辑是否委托给 service/repository,而不是塞在 router 里?

4.2 链路二:Public REST API 供 SDK 和外部系统调用

SDK 或外部系统调用 /api/public/**,通常走 Next.js API route。

源码锚点:

和 tRPC 的区别:

对比项tRPCPublic REST API
使用者Langfuse Web UISDK、外部集成、用户脚本
鉴权NextAuth session + project membershipAPI 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 采用“先收下、再异步处理”的模型。

源码锚点:

关键直觉:

  • 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) 看:

  1. 这个 queue 是否有 env 开关?
  2. concurrency、limiter、lockDuration、maxStalledCount 怎么设?
  3. processor 的输入 payload 是否来自 packages/shared/src/server/queues.ts
  4. processor 成功和失败分别代表什么?失败是否应该重试?

4.5 链路五:v4 events 查询从 FilterState 变成 ClickHouse SQL

v4 events 表把 observation/trace 相关属性 denormalize 到宽事件中。前端筛选、搜索栏、表格列和后端查询需要共享同一套语言。

源码锚点:

这条链路的关键不是 UI,而是契约一致性:搜索栏只是 FilterState 的编辑器,不能绕过 filter contract 自己发明一种后端不懂的查询语言。


5. 契约中心:为什么 shared 是关键

5.1 契约的类型

Langfuse 里常见的契约不是单一接口,而是一组组合规则:

契约文件例子解决的问题
domain schemapackages/shared/src/domain/**repositories/definitions.ts运行时数据长什么样
queue payloadpackages/shared/src/server/queues.tsweb 入队和 worker 消费必须一致
filter statepackages/shared/src/interfaces/filters.tsUI 筛选和后端查询共享语言
query builderpackages/shared/src/server/queries/**避免散落手写 SQL
repositorypackages/shared/src/server/repositories/**复杂数据读取、转换、tenant filter
public API typeweb/src/features/public-api/types/**外部 API 的请求/响应稳定性
UI table contractweb/src/components/table/**列定义、排序、可见性、分页、行选择

5.2 Zod + TypeScript 的模式

很多数据结构会同时具备:

  1. Zod schema,负责运行时校验。
  2. z.infer<> type,负责 TypeScript 静态类型。
  3. 转换函数,负责 DB record 和 domain model 之间的映射。

例子:

  • observationRecordReadSchema
  • traceRecordInsertSchema
  • scoreRecordInsertSchema
  • eventRecordBaseSchema
  • eventRecordInsertSchema

这种模式的价值是:数据从外部进入系统时能被校验,进入代码后又有 TS 类型跟踪。

5.3 queue contract 是 web 和 worker 的共同边界

packages/shared/src/server/queues.ts 里有三个层次:

  1. payload schema,例如 IngestionEventOtelIngestionEventEvalExecutionEvent
  2. queue name,例如 QueueName.IngestionQueue
  3. job name 和类型映射,例如 QueueJobs.IngestionJobTQueueJobTypes

为什么这么设计?

  • 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_id filter:避免租户数据串读。
  • 对 events 表排序做优化:按 project_idtoStartOfMinute(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 路径:

角色
tracesobservationsscoreslegacy ClickHouse 表
observations_batch_staging批量 staging,带 TTL
events_fullv4 全量保真事件表,包含完整 input/output/metadata
events_corev4 轻量查询投影,面向列表和常用查询

events_full 的设计重点:

  • 宽事件,一行携带 trace、span、model、usage、cost、metadata、tool、experiment 等上下文。
  • 使用 ReplacingMergeTree(event_ts, is_deleted)
  • 主键和排序围绕 project_idtoStartOfMinute(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 会包含:

text
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

源码锚点:

读表格代码时,要区分:

  • 表格容器负责数据请求和 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 暴露。

路线:

  1. 在对应 web/src/features/[feature]web/src/server/api/routers 找现有 router。
  2. 定义 Zod input schema。
  3. 使用 protectedProjectProcedure 或其他合适 procedure。
  4. 把业务逻辑放到 service/repository,不要写满 router。
  5. 需要复杂 ClickHouse 读写时优先放 shared repository。
  6. 前端通过 api.[router].[procedure].useQuery/useMutation 调用。
  7. 写 targeted web test。

自检:

  • 输入里是否有 projectId
  • 是否校验 project membership?
  • 是否复用 shared 契约?
  • 是否把 business logic 塞进 route 了?

8.2 新增 Public API endpoint

适合场景:SDK、外部用户、自动化脚本需要稳定调用。

路线:

  1. web/src/pages/api/public/** 建 route。
  2. 使用 withMiddlewares 包住 HTTP method。
  3. 使用 createAuthedProjectAPIRoute 做鉴权、限流、Zod 校验。
  4. request/response schema 放到 web/src/features/public-api/types/**
  5. 响应也要被 Zod 校验,避免返回未声明字段。
  6. 更新 Fern sources。
  7. 更新 generated client 或相关 SDK 类型。
  8. 写 server API 测试。

自检:

  • 这是不是公共契约?如果是,不能只改 web route。
  • response schema 是否 strict?
  • 错误是否使用 shared error 类型?
  • Fern/API docs 是否同步?

8.3 新增 worker 队列任务

适合场景:耗时、可重试、批量、调用外部系统或不应阻塞 HTTP 请求的工作。

路线:

  1. packages/shared/src/server/queues.ts 定义 payload schema。
  2. 添加 QueueNameQueueJobsTQueueJobTypes 映射。
  3. 在合适 producer 里 add job。
  4. worker/src/queues/** 写 processor。
  5. worker/src/app.tsWorkerManager.register 注册。
  6. 设置 concurrency、limiter、lockDuration、maxStalledCount。
  7. 写 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。

路线:

  1. 先看 packages/shared/src/server/ingestion/types 和 domain/repository definitions。
  2. 修改 event record schema。
  3. 修改 ClickHouse migration 或 dev table。
  4. 修改 IngestionService 转换逻辑。
  5. 修改 ClickhouseWriter TableName 或 record type,如果新增表。
  6. 修改 query builder field mapping。
  7. 修改 UI column/filter/search registry。
  8. 增加 ingestion worker 测试和查询测试。

自检:

  • 字段是 full fidelity 还是 core projection 也需要?
  • 是否应该 denormalize?
  • 是否会扫描大字段?
  • 是否需要 migration/backfill?
  • legacy 和 v4 双写模式下行为是否一致?

8.5 修改 Search Bar 或筛选语法

必须先读 web/src/features/search-bar/README.md

路线:

  1. 明确 FilterState 是否能表达新语义。
  2. 如果不能表达,不要只改 parser。
  3. 同步更新 field registry、validate、adapter、reverse adapter、commit gate。
  4. 保持 validate 和 lower parity。
  5. 增加 property/invariant 测试。
  6. 检查 URL round-trip。

自检:

  • 是否会静默丢 filter?
  • sidebar 和 search bar 是否仍读写同一事实源?
  • negation 是否能 lower 到现有 inverse operator?
  • free text search 的 scope 是否仍一致?

9. 读代码练习:从任务倒推源码

9.1 练习一:追踪一次 UI trace 列表查询

目标:弄清楚 trace list 如何从 UI 变成 ClickHouse 查询。

步骤:

  1. 从 trace 页面或 tracing table 组件开始找 api.traces 或 events table 调用。
  2. 找到 tRPC router:web/src/server/api/root.tstraces: traceRouter
  3. 找到具体 procedure。
  4. 看 input schema 是否包含 projectId、filter、orderBy、pagination。
  5. 找到 shared repository 或 query builder。
  6. 确认 ClickHouse 查询是否带 project_id 和时间范围。

你要能回答:

  • 这个列表读 legacy 表还是 v4 events?
  • 哪些字段是列表页需要的?
  • input/output 是否被延迟读取?

9.2 练习二:追踪一次 SDK ingestion

目标:从 HTTP 请求追到 ClickHouse 写入。

步骤:

  1. 找 ingestion API route。
  2. 找它如何调用 processEventBatch
  3. processEventBatch 中看校验、分组、S3 上传和入队。
  4. queues.ts 中看 payload 契约。
  5. worker/src/app.ts 中看 ingestion queue 是否注册。
  6. worker/src/queues/ingestionQueue.ts 中看 S3 读取、secondary queue、mergeAndWrite。
  7. IngestionService 中看事件如何转为 record。
  8. ClickhouseWriter 中看批量写入。

你要能回答:

  • 为什么 Redis job 不直接塞完整 event body?
  • S3 SlowDown 时为什么有 secondary queue?
  • 为什么 worker 要从 event body 取 canonical entity id?

9.3 练习三:新增一个 Public API 字段

目标:理解公共契约的修改面。

步骤:

  1. 找对应 web/src/pages/api/public/** route。
  2. 找 response schema。
  3. 找 service/repository。
  4. 找 Fern API 定义。
  5. 找 generated client 或 SDK 类型影响。
  6. 写 API 测试校验 response schema。

你要能回答:

  • 这个字段来自 Postgres 还是 ClickHouse?
  • 是否会触发大字段读取?
  • 旧客户端看到这个字段是否安全?

9.4 练习四:判断一个变化应该放哪层

变化应该先看
新增一个项目设置项Postgres schema、settings UI、tRPC router
新增一种后台清理任务queues.tsworker/src/app.ts、worker processor
新增 observation 过滤字段FilterState、field registry、query builder、ClickHouse schema
新增表格列table column definition、query field set、repository
新增 SDK endpointPublic API route、types、Fern、SDK
新增 ingestion event 字段ingestion types、IngestionService、ClickHouse schema、query builder

10. 术语表与常见误区

10.1 术语表

术语在本 repo 中的意思
webNext.js app,包含 UI、tRPC、Public API、ingestion HTTP 入口
workerBullMQ consumer 和后台任务进程
sharedweb/worker/ee 共享的契约、领域、仓储、服务
tRPCWeb UI 内部类型安全 API
Public API面向 SDK 和外部用户的 REST API
protectedProjectProceduretRPC 的项目级 session/RBAC 中间件
createAuthedProjectAPIRoutePublic API 的 API key 鉴权和 Zod 校验包装器
FilterState表格筛选的统一状态契约
events_fullv4 full fidelity ClickHouse event 表
events_corev4 query-optimized event 投影
ClickhouseWriterworker 内部批量写 ClickHouse 的单例
WorkerManagerworker 队列注册和管理入口
FernPublic 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 最小上手命令

bash
pnpm install
pnpm run infra:dev:up
pnpm run db:generate
pnpm run dev:web
pnpm run dev:worker

常用验证:

bash
pnpm run lint
pnpm run typecheck
pnpm --filter web run test
pnpm --filter worker run test
pnpm --filter @langfuse/shared run test

种子数据:

bash
pnpm run seed -- list

涉及 UI 的改动,优先使用 seed CLI 生成场景,然后在浏览器里检查真实页面。


最后总结

读 Langfuse 的核心路线可以压缩成一句话:

外部请求进入 web,稳定规则沉到 shared,耗时任务交给 worker,事务数据进 Postgres,分析事件进 ClickHouse,大 payload 放 S3,队列和限流交给 Redis。

进一步拆开,就是四个判断:

  1. 这是同步产品/API 请求,还是异步后台任务?
  2. 这是内部 UI 契约,还是外部 Public API 契约?
  3. 这个数据是事务配置,还是高吞吐分析事件?
  4. 这个规则应该属于入口层,还是应该沉到 shared 供 web/worker 共同使用?

只要这四个判断清楚,修改 Langfuse 的大多数路径都能落到正确位置。