可观测性
面向维护者。正在使用 T3 Code?请参阅 用户文档。
T3 Code 只有一套服务端可观测性模型:
- 美化日志输出到 stdout,供人阅读
- 已完成的跨度(span)写入本地 NDJSON 追踪文件
- 追踪、指标和日志也可以通过 OTLP 导出到真正的后端,例如 Grafana LGTM
对普通的本地启动来说,本地追踪文件是持久化后的事实来源。这类启动不会另写一份服务器日志文件,但由 SSH 管理的启动还会把远程进程的 stdout/stderr 持久化到 ~/.t3/ssh-launch/<state>/server.log。
各项内容在哪里
日志
日志是给人看的:
- 输出目标:stdout
- 格式:
Logger.consolePretty() - 普通本地持久化:无
- SSH 管理的启动持久化:
~/.t3/ssh-launch/<state>/server.log - 远程导出:仅 OTLP,且需已配置
如果希望一条日志出现在追踪文件里,请在活动跨度内用 Effect.log... 打出它。Logger.tracerLogger 会把它挂成一条跨度事件(event)。
配置了日志端点之后,这项工作就由它接管。服务器随后导出日志记录:覆盖每一条消息,而不只是活动跨度里的那些,并带上追踪 ID 和跨度 ID,因此仍能与追踪对齐。该模式下会去掉 Logger.tracerLogger,这样同一条消息不会被导出两次,追踪文件也不再携带日志消息。无论是否配置,stdout 输出和 SSH 管理的启动持久化都保持不变。
追踪
已完成的跨度会作为 NDJSON 记录写入 serverTracePath。默认路径取决于服务器如何启动:生产环境以及显式配置的主目录使用 <home>/userdata/logs/server.trace.ndjson(因此默认是 ~/.t3/userdata/...,或为 /custom/path/userdata/...,并配合 --home-dir /custom/path);链接工作树(worktree)中的开发运行使用 <worktree>/.t3/userdata/logs/server.trace.ndjson;链接工作树之外的隐式开发运行使用 ~/.t3/dev/logs/server.trace.ndjson。
两种记录类型共有的重要字段:
type:effect-span或otlp-spanname:跨度名称traceId、spanId、parentSpanId:关联durationMs:耗时attributes:结构化上下文events:内嵌日志与自定义事件
effect-span 记录还包含 exit,取值为 Success、Failure 或 Interrupted。otlp-span 记录则改为携带 OTLP 的资源、作用域,以及可选的状态字段。
TraceRecord、EffectTraceRecord 和 OtlpTraceRecord 的模式(schema)位于 packages/shared/src/observability.ts。
名为 sqlite 的 SQL 语句跨度,以及 sql.transaction 跨度,会携带 db.connection 和 db.lock_wait_ms。db.connection 在主连接上为 writer,这条连接跑在事件循环上;在工作线程的只读连接上则为 reader。db.lock_wait_ms 是该跨度中花在等待这条连接上的部分,也就是排在其他 SQL 后面的那段等待。这段等待不会堵住事件循环,所以在把一条慢跨度归咎于查询之前,先把它减掉。事务内的语句沿用该事务的连接,不会带上这些属性。
经 HTTP 进来的 MCP 请求有一条 McpHttpServer.authenticateRequest 跨度,只覆盖准入本身:凭据检查、模式限制,以及已链接环境门禁。请求本身在这之后才执行,位于 McpServer.…/<method> 跨度之下;每次工具调用另有自己的 mcp.tool.<name> 跨度来计时。这条跨度的属性只写工具名,绝不写它的参数。
DPoP 证明失败会带上可安全记录的 environment.dpop.failure_code 跨度属性。time_window 失败表示,已签名的证明相对环境(environment)服务器允许的时间窗口来说太旧,或者太超前。这可能说明某一端设备的日期或时间有问题,但也可能只是请求迟到了。
汇总追踪文件
t3 trace summary 直接读取追踪文件及其轮转备份,因此服务器卡住或已经停止时它仍然可用。它按跨度名称打印计数、速率和延迟百分位。用它来衡量后台工作,或比较两次构建。
t3 trace summary --since 30m --limit 40若设置了 T3CODE_TRACE_FILE 就读取它,否则读取 <home>/userdata/logs/server.trace.ndjson(用于 --base-dir 或 T3CODE_HOME),并加上 T3CODE_TRACE_MAX_FILES 这些轮转备份。开发运行,或者文件是复制来的,请设置 T3CODE_TRACE_FILE。--since 30m 只保留最近 30 分钟内结束的跨度。速率按第一段跨度结束到最后一段跨度结束之间的每分钟计算。
指标
指标不会写入本地文件。
- 本地持久化:无
- 远程导出:仅 OTLP,且需已配置
- 当前定义:
apps/server/src/observability/Metrics.ts
如果没有配置 OTLP,指标仍然存在于进程内,但你不会有一份可以检查的本地产物。
事件循环停顿
apps/server/src/observability/EventLoopMonitor.ts 每 30 秒对服务器的事件循环采样一次。自上次采样以来,如果事件循环停顿超过 2 秒,它会记录一条根 server.eventLoop.stall 跨度,并附一条警告。该跨度的追踪级别为 Warn,因此当 T3CODE_TRACE_MIN_LEVEL 为 Warn 时仍会保留。这条警告会显示在「设置 > 诊断」中,除非已经开启 OTLP 日志。跨度上的时间是采样运行的时刻,不是停顿发生的时刻。
在繁忙的服务器上,追踪文件远不到一小时就会轮转,因此每次停顿还会以一行 NDJSON 追加到追踪文件旁边的 event-loop-stalls.ndjson。每一行包含 time 和跨度属性。该文件保留最近 1000 条或更多停顿,并有一份轮转备份(event-loop-stalls.ndjson.1)。用它查看一台服务器在数天里停顿的频繁程度。
有些延迟不会被记录:
delayMaxMs是最长的一次停顿,并且最多可能少计 1 秒。2 秒阈值针对的就是这个值,因此超过 3 秒的停顿通常会被记录,更短的则可能漏掉。刚好在某次采样运行时结束的停顿,也可能漏掉。- 计算机处于休眠的时间,在 macOS 和 Windows 上会被读成延迟。因此,只有事件循环处于忙碌、而不是在等待事件,并且至少达到
delayMaxMs时,这次采样才会计入。忙碌时间覆盖整个窗口,所以在一个原本忙碌的窗口里短暂休眠,仍可能记成一次虚假停顿。这时跨度上的 CPU 时间会远低于delayMaxMs。 - 启动后的第一次采样会被跳过。迁移和投影(projection)引导这类启动工作,在大型数据库上可能把事件循环堵住数秒。
CPU 时间和缺页统计的是自上次采样以来、整个窗口内的整个进程。窗口名义上是 30 秒,但长时间停顿会推迟采样,把窗口拉长。窗口内的其他工作可能把一次等待掩盖掉,因此只有 CPU 时间远低于 delayMaxMs,才能证明该线程当时在等待。请把 CPU 和缺页放在一起看:
cpuSystemMs高,同时缺页很多,表示内存压力。主要缺页是从磁盘或交换区读取。在 macOS 上,从压缩内存读取表现为次要缺页,再加上系统 CPU。cpuUserMs高,同时缺页很少,表示 JavaScript 工作或垃圾回收。- CPU 低,同时主要缺页很少,指向同步磁盘 I/O,例如 SQLite 读取或追踪文件写入。
involuntaryContextSwitches很多,表示其他进程在争抢 CPU。
相关产物
提供方(provider)运行时流仍然各有自己的提供方事件 NDJSON 文件。它们与主服务器追踪文件是分开的。
以插桩模式运行服务器
有两种有用的模式:
- 仅本地:stdout + 本地
server.trace.ndjson - 完整的本地可观测性:stdout + 本地追踪文件 + 通过 OTLP 导出到 Grafana/Tempo/Prometheus
本地追踪文件始终开启。OTLP 导出需要主动启用。
方案 1:仅本地追踪
不需要任何额外的环境变量。正常运行应用,然后检查 server.trace.ndjson 即可。
示例:
t3node --run devnode --run dev:desktop方案 2:配合本地 LGTM 技术栈运行
1. 启动 Grafana LGTM
docker run --name lgtm \
-p 3000:3000 \
-p 4317:4317 \
-p 4318:4318 \
--rm -ti \
grafana/otel-lgtm然后打开 http://localhost:3000。
Grafana 的默认登录:
- 用户名:
admin - 密码:
admin
2. 导出 OTLP 环境变量
端点设置以及启动时的优先级,见用户遥测指南。在工作树(worktree)中,用 vp run dev 代替 t3,让状态保持隔离。
export T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces
export T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics
export T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=development可选:
export T3CODE_TRACE_MIN_LEVEL=Info
export T3CODE_TRACE_TIMING_ENABLED=true3. 从同一个 shell 启动应用
命令行:
t3单体仓库(monorepo)的 Web/服务器开发:
node --run dev单体仓库的桌面开发:
node --run dev:desktop打包后的桌面应用:
从同一个 shell 启动真正的应用可执行文件,这样桌面应用和内嵌后端都会继承 T3CODE_OTLP_*。
macOS 应用包示例:
T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
"/Applications/T3 Code.app/Contents/MacOS/T3 Code"直接运行二进制文件的示例:
T3CODE_OTLP_TRACES_URL=http://localhost:4318/v1/traces \
T3CODE_OTLP_METRICS_URL=http://localhost:4318/v1/metrics \
T3CODE_OTLP_LOGS_URL=http://localhost:4318/v1/logs \
./path/to/your/desktop-app-binary设置了 shell 环境变量之后,不要依赖从 Finder、Spotlight、程序坞或开始菜单来启动。这些启动方式通常带不上这些变量。
4. 更改环境变量后完全重启
后端在进程启动时读取可观测性配置。如果改了 OTLP 环境变量,请把应用完全停掉,然后再启动。
如何用追踪和指标调试服务器
从本地追踪文件开始
追踪文件是查看原始跨度(span)数据最快的办法。
先按启动模式把路径确定一次。生产环境以及显式配置的主目录,把运行时状态放在基目录的 userdata 文件夹下:
TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson"从链接工作树(worktree)启动的开发服务器,默认使用该工作树自己的本地主目录:
TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson"只有链接工作树之外的隐式开发运行,才使用共享的开发目录:
TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson"跟踪所选文件:
tail -f "$TRACE_FILE"显示失败的跨度:
jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | {
name,
durationMs,
exit,
attributes
}' "$TRACE_FILE"显示慢跨度:
jq -c 'select(.durationMs > 1000) | {
name,
durationMs,
traceId,
spanId
}' "$TRACE_FILE"检查内嵌的日志事件(event):
jq -c 'select(any(.events[]?; .attributes["effect.logLevel"] != null)) | {
name,
durationMs,
events: [
.events[]
| select(.attributes["effect.logLevel"] != null)
| {
message: .name,
level: .attributes["effect.logLevel"]
}
]
}' "$TRACE_FILE"顺着一条追踪看下去:
jq -r 'select(.traceId == "TRACE_ID_HERE") | [
.name,
.spanId,
(.parentSpanId // "-"),
.durationMs
] | @tsv' "$TRACE_FILE"筛选编排命令(command):
jq -c 'select(.attributes["orchestration_v2.command_type"] != null) | {
name,
durationMs,
commandType: .attributes["orchestration_v2.command_type"],
threadId: .attributes["orchestration_v2.thread_id"]
}' "$TRACE_FILE"筛选 Git 活动:
jq -c 'select(.attributes["git.operation"] != null) | {
name,
durationMs,
operation: .attributes["git.operation"],
cwd: .attributes["git.cwd"],
hookEvents: [
.events[]
| select(.name == "git.hook.started" or .name == "git.hook.finished")
]
}' "$TRACE_FILE"需要真正的追踪查看器时使用 Tempo
想做下面这些事时,Tempo 比原始 NDJSON 更合适:
- 在大量追踪里搜索
- 直观查看父子关系
- 比较许多条慢追踪
- 深入查看一次失败的请求,而不必按
traceId手工拼接
Grafana 中的推荐流程:
- 打开
Explore。 - 选择
Tempo数据源。 - 把时间范围设成较近的一段时间,例如
Last 15 minutes。 - 先从宽处查起。不要一开始就用非常窄的查询。
- 先找来自
t3code-server或t3code-desktop服务的跨度,再按跨度名称或属性收窄。
适合作为第一步的搜索:
- 服务名为
t3code-server或t3code-desktop,再加一个资源属性,例如deployment.environment.name - 跨度名称,例如
sendTurn,或者一项 Git 操作,例如GitVcsDriver.statusDetails.status git.operation属性标明了具体操作的 Git 跨度- 带有
orchestration_v2.command_type这类属性的编排跨度
确认追踪已经到达之后,针对 sendTurn 这类名称或 Git 操作名的更窄 TraceQL 查询就会有用。
用指标发现系统性问题
追踪最适合看单次请求。指标最适合看趋势。
值得关注的指标族:
t3_rpc_request_durationt3_orchestration_command_durationt3_provider_turn_duration(提供方(provider)适配器(adapter)启动一个回合(turn)要多久,不是这个回合自己的运行时长)t3_git_command_duration
计数器告诉你数量和失败率:
t3_rpc_requests_totalt3_orchestration_commands_totalt3_provider_turns_totalt3_git_commands_total
Webhook 有自己的指标族:
t3_webhook_deliveries_total按outcome和source划分(relay或direct)。除发送方自己能看到的情况外,queue_full表示某个任务等待中的投递已经达到上限,prompt_too_long表示填好的提示词超过了提供方限制,duplicate表示中继(relay)投递了一个该环境(environment)已经运行过的请求。t3_webhook_runs_total按outcome划分(started、skipped、failed),对应这些投递所启动的运行;它们发生在发送方已经拿到答复之后。t3_webhook_held_delay表示中继暂存的请求在到达之前等待了多久。t3_secret_requests_total按status划分(saved、declined、cancelled、timed_out),对应智能体向用户索要的机密;t3_secret_refs_consumed_total按result划分(used、rejected),对应兑换这些机密的工具。两者都从不携带值。
ScheduledTaskService.triggerWebhook 跨度按每个请求携带同样的结果,而每一次由投递启动的运行都是一条独立的 ScheduledTaskService.runWebhookDelivery 追踪。中继转发的请求,其跨度还会作为中继那条跨度的子跨度,进入 T3 Connect 的追踪导出;直接到达环境的请求,永远不会并入发送方的追踪。
问题是下面这些时,用指标:
- 「是不是一直都慢?」
- 「改过之后是不是更糟了?」
- 「哪种命令类型失败得最频繁?」
问题是下面这些时,用追踪:
- 「这一次具体的请求里发生了什么?」
- 「是哪个子跨度造成了这一次变慢的交互?」
- 「失败的这条流程里打出了哪些日志?」
常见工作流
「这次请求为什么失败了?」
- 从本地 NDJSON 文件开始。
- 找出
effect-span记录中满足exit._tag != "Success"的那些。 - 按
traceId分组。 - 检查同级跨度(span)和跨度事件(event)。
- 如有需要,转到 Tempo 查看完整的追踪树。
「为什么界面感觉变慢了?」
- 在追踪文件或 Tempo 里搜索较慢的顶层跨度。
- 检查子跨度中的 SQLite、Git、提供方(provider)或终端工作。
- 查看对应的耗时指标,判断这种变慢是不是系统性的。
「是 Git 钩子在造成延迟吗?」
- 筛选
git.operation跨度。 - 检查
git.hook.started和git.hook.finished事件。 - 把钩子的耗时与包住它的 Git 跨度时长作比较。
「为什么本地有跨度,Grafana 里却什么都没有?」
通常是下面某一条成立:
- 没有设置
T3CODE_OTLP_TRACES_URL - 应用是从另一个环境启动的,不是你导出这些变量的那个环境
- 更改环境变量之后没有完全重启应用
- Grafana 查看的时间范围或服务名不对
如果本地 NDJSON 文件还在更新,说明本地追踪是正常的。问题几乎总是出在 OTLP 导出配置,或进程的启动方式上。
以后给代码加追踪时该怎么想
优先划在边界上,而不是细碎的辅助函数
合适的跨度(span)边界:
- RPC 方法
- 编排命令(command)的处理
- 提供方(provider)适配器(adapter)调用
- 外部进程调用
- 持久化写入
- 队列交接
不要给每一个细碎的辅助函数都打追踪。大多数辅助函数应当继承当前的活动跨度,而不是自己再开一个。
已经有 Effect.fn(...) 的地方就复用它
代码库里已经大量使用 Effect.fn("name")。它通常应当作为你的第一道追踪边界。
临时要打的追踪:
import { Effect } from "effect";
const runThing = Effect.gen(function* () {
yield* Effect.annotateCurrentSpan({
"thing.id": "abc123",
"thing.kind": "example",
});
yield* Effect.logInfo("starting thing");
return yield* doWork();
}).pipe(Effect.withSpan("thing.run"));把高基数细节放在跨度上
ID、路径以及其他详细上下文,用跨度标注来放:
yield *
Effect.annotateCurrentSpan({
"provider.thread_id": input.threadId,
"provider.request_id": input.requestId,
"git.cwd": input.cwd,
});让指标标签保持低基数
合适的指标标签:
- 操作类型
- 方法名
- 提供方类型
- 聚合类型
- 结果
不合适的指标标签:
- 原始的对话线程(thread)ID
- 命令 ID
- 文件路径
- cwd
- 完整提示词
- 已经有规范化的系列标签时,仍使用完整的模型字符串
详细上下文属于跨度,不属于指标。
把日志当作跨度事件
打在跨度里的日志,会成为这条追踪的一部分:
yield * Effect.logInfo("starting provider turn");
yield * Effect.logDebug("waiting for approval response");因为安装了 Logger.tracerLogger,这些消息会以跨度事件(event)的形式出现。
使用可管道组合的指标 API
withMetrics(...) 是给一个效应挂上计数器和计时器的默认做法:
import { someCounter, someDuration, withMetrics } from "../observability/Metrics.ts";
const program = doWork().pipe(
withMetrics({
counter: someCounter,
timer: someDuration,
attributes: {
operation: "work",
},
}),
);详细 API 参考
运行时装配
服务器的可观测性层在 apps/server/src/observability/Observability.ts 中装配。
它提供:
- 美化的 stdout 日志器
Logger.tracerLogger- 本地 NDJSON 追踪器
- 可选的 OTLP 追踪导出器
- 可选的 OTLP 指标导出器
- 可选的 OTLP 日志导出器
- Effect 的追踪级别引用与计时引用
桌面主进程是第二个生产方,在 apps/desktop/src/app/DesktopObservability.ts 中装配。它读取与所监管的后端相同的 T3CODE_OTLP_* 名称,以及相同的设置项,并覆盖后端看不到的工作:应用启动、窗口与菜单处理、后端监管,以及更新。它以服务 t3code-desktop 上报,因此收集器会把它与后端并列显示,而不是混进后端里。它只导出追踪和日志;主进程不记录指标,所以指标端点只作用于后端。
环境变量
本地追踪文件:
T3CODE_TRACE_FILE:覆盖追踪文件路径T3CODE_TRACE_MAX_BYTES:单个文件的轮转大小,默认10485760T3CODE_TRACE_MAX_FILES:轮转文件的数量,默认10T3CODE_TRACE_BATCH_WINDOW_MS:刷盘窗口,默认1000T3CODE_TRACE_MIN_LEVEL:最低追踪级别,默认InfoT3CODE_TRACE_TIMING_ENABLED:是否启用计时元数据,默认true
OTLP 导出:
T3CODE_OTLP_TRACES_URL:OTLP 追踪端点T3CODE_OTLP_METRICS_URL:OTLP 指标端点T3CODE_OTLP_LOGS_URL:OTLP 日志端点T3CODE_OTLP_EXPORT_INTERVAL_MS:导出间隔,默认10000T3CODE_OTLP_HEADERS:三个导出器共用的附加请求头,格式与OTEL_EXPORTER_OTLP_HEADERS相同,即逗号分隔的key=value对,值为百分号编码。T3CODE_OTLP_PROTOCOL:http/json(默认)或http/protobuf
服务器和桌面应用(desktop app)还会读取标准的 OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT,以及通用的 OTEL_EXPORTER_OTLP_ENDPOINT(后面追加 /v1/traces、/v1/metrics 或 /v1/logs),以便适配期望这些变量的收集器。非空白的 T3CODE_OTLP_*_URL 优先于这两者;某个信号自己的端点,又优先于该信号的通用端点。空白值视为未设置。带有 OTEL 端点的信号,其请求头取自 OTEL_EXPORTER_OTLP_HEADERS,协议取自 OTEL_EXPORTER_OTLP_PROTOCOL(默认 http/protobuf,读取时不区分大小写);该信号自己的 OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_HEADERS 或 _PROTOCOL 优先于通用项。T3CODE_OTLP_HEADERS 和 T3CODE_OTLP_PROTOCOL 对它从不生效。如果端点不是 http 或 https 形式的 URL,协议也不是 http/protobuf 或 http/json(例如 grpc),或者请求头不是带百分号编码值的 key=value 对,该信号的导出就会关闭,并给出一条启动警告,而不会改发到设置中的端点。
服务名是固定的:后端为 t3code-server,桌面主进程为 t3code-desktop,两者都写在 service.namespace t3code 里。OTEL_SERVICE_NAME,以及 service.name 或 service.namespace(写在 OTEL_RESOURCE_ATTRIBUTES 里),都会被忽略。要区分不同的安装,请用其他资源属性,例如 OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=staging。
每个进程上报的是自己的构建,而不是它正在通信的那一端:服务器、桌面主进程、UI 和移动应用(mobile app)各自携带 t3code.release.channel(取值为 dev、stable、nightly 或 preview,来自各自的版本;桌面 PR 构建算作 preview,不是由发布工作流构建的服务器则上报 dev)以及 deployment.environment.name(development、production 或 staging,对应 preview)。在服务器和桌面主进程上,OTEL_RESOURCE_ATTRIBUTES 中的任一键都优先。UI 和移动应用从不读取这个变量:Vercel 的 PR 预览会设置 VITE_RELEASE_CHANNEL=preview,因为它们的 APP_VERSION 必须与服务器的保持可比;移动应用则映射自己的构建变体(development、preview、production),或者从开发包上报 dev。
T3 Code 专有的属性使用 t3code. 前缀。UI 在浏览器和桌面窗口中都以 t3code-web 运行,因为是同一套代码,而 t3code.client.surface 为 desktop 或 web。当桌面应用拉起服务器时,服务器的 t3code.server.managed_by 为 desktop,否则为 standalone。桌面主进程把 t3code.desktop.mode 报为 development 或 packaged(原先为 service.mode;按该键过滤的仪表盘必须改过来)。Node 进程还会上报语义约定中的 process.runtime.name、process.runtime.version 和 process.runtime.description。UI 追踪会上报 browser.language 和 user_agent.original;在 Chromium 中,还会上报 browser.platform、browser.mobile 和 browser.brands。
每份资源还会携带一个 service.instance.id:服务器和桌面端按进程随机生成,UI 按每次页面加载生成,移动端则使用 Expo 会话。service.instance.id(位于 OTEL_RESOURCE_ATTRIBUTES 中)会在服务器和桌面端替换掉生成的那个值。
如果没有设置 OTLP 的 URL,本地追踪仍然可用,指标只留在进程内,日志只留在 stdout。
紧急开关
T3CODE_OTEL_SDK_DISABLED 和 OTEL_SDK_DISABLED 会关闭服务器与桌面主进程中的全部 OTLP 导出,并覆盖来自环境变量或设置的任何端点。本地追踪文件和 stdout 日志不受影响。
一旦设置了 T3CODE_OTEL_SDK_DISABLED,就由它优先,因此 T3CODE_OTEL_SDK_DISABLED=false 会在一台为其他所有程序都设置了 OTEL_SDK_DISABLED 的机器上重新启用导出。它接受常见的布尔写法(true/false、yes/no、on/off、1/0、y/n)。OTEL_SDK_DISABLED 遵循 OpenTelemetry 规范,只有 true 会禁用导出,所以 OTEL_SDK_DISABLED=1 不会。值不区分大小写,并会去掉首尾空白。无法识别的值会被忽略,同时给出一条启动警告。
把 OTEL_TRACES_EXPORTER、OTEL_METRICS_EXPORTER 或 OTEL_LOGS_EXPORTER 设为 none,只会关闭这一种信号,并覆盖 OTEL 端点以及设置中的端点。对应信号的 T3CODE_OTLP_*_URL 仍然优先。otlp 是默认值;任何其他导出器名称,例如 console 或 prometheus,都会被忽略,同时给出一条启动警告。
当前已插桩的内容
当前高价值的跨度(span)与指标边界包括:
- WebSocket RPC 请求跨度(
ws.rpc.<method>),以及apps/server/src/observability/RpcInstrumentation.ts中的指标 - 启动阶段
- 编排命令(command)的处理
- 提供方会话(session)与回合(turn)操作
- Git 命令执行与 Git 钩子事件(event)
- 终端会话的生命周期
- SQLite 查询执行,但
orchestrationV2.EventSink.write内部的语句除外,它们由其sql.transaction跨度作为一整段来计时 - 事件循环停顿(
server.eventLoop.stall)
当前限制
- 跨度之外的日志不会持久化到追踪文件中;由 SSH 管理的启动,其 stdout/stderr 仍会记入它的启动器日志
- 指标不会在本地做快照
堆快照
要查看一台长时间运行的服务器在内存里持有什么,向它发送 SIGUSR2。服务器会把一份 V8 堆快照写到自己的日志目录,并把路径打到日志里。桌面安装、npx t3,以及 macOS 和 Linux 上以服务方式安装的实例都支持。Windows 没有 SIGUSR2。
把信号发给 server-runtime.json 中的服务器 pid。该文件位于服务器的状态目录里,与 logs 目录相邻。开发服务器或 --home-dir 启动,请使用追踪一节里该服务器的状态目录。不要把信号发给桌面应用(desktop app)或服务启动器:没有对应处理逻辑的进程,收到 SIGUSR2 就会退出。崩溃之后,文件里可能留着一个已经属于另一个进程的过期 pid,所以要先核对 pid。
pid="$(jq .pid "${T3CODE_HOME:-$HOME/.t3}/userdata/server-runtime.json")"
ps -p "$pid" -o command=如果 ps 显示的是 T3 Code 服务器,再发送信号:
kill -USR2 "$pid"文件为 <logsDir>/server-<pid>-<timestamp>.heapsnapshot,位于 server.trace.ndjson 旁边。要打开它,使用 Chrome DevTools 的内存(Memory)面板,然后选择加载(Load)。
拍一份之前:
- 写入文件期间,服务器会停住。堆比较大时,可能要一分钟甚至更久。暂停期间,已连接的客户端(client)可以重新连上;如果这台服务器有事件循环监视器,会把这次暂停记成一次停顿。信号只发一次。写入过程中若再发一次信号,会在第一次写完之后再做一份快照。
- 写入大约需要与堆占用相当的空闲内存。机器如果已经在换页,这样做可能让问题更严重,甚至让服务器崩溃。
- 文件里是服务器内存中的全部内容,包括令牌、机密,以及对话线程(thread)的内容。不要公开分享。用完就删掉,因为存储清理不会移除它。