T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

可观测性 ​

面向维护者。正在使用 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-span
  • name:跨度名称
  • 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 直接读取追踪文件及其轮转备份,因此服务器卡住或已经停止时它仍然可用。它按跨度名称打印计数、速率和延迟百分位。用它来衡量后台工作,或比较两次构建。

bash
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 即可。

示例:

bash
t3
bash
node --run dev
bash
node --run dev:desktop

方案 2:配合本地 LGTM 技术栈运行 ​

1. 启动 Grafana LGTM ​

bash
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,让状态保持隔离。

bash
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

可选:

bash
export T3CODE_TRACE_MIN_LEVEL=Info
export T3CODE_TRACE_TIMING_ENABLED=true

3. 从同一个 shell 启动应用 ​

命令行:

bash
t3

单体仓库(monorepo)的 Web/服务器开发:

bash
node --run dev

单体仓库的桌面开发:

bash
node --run dev:desktop

打包后的桌面应用:

从同一个 shell 启动真正的应用可执行文件,这样桌面应用和内嵌后端都会继承 T3CODE_OTLP_*。

macOS 应用包示例:

bash
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"

直接运行二进制文件的示例:

bash
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 文件夹下:

bash
TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson"

从链接工作树(worktree)启动的开发服务器,默认使用该工作树自己的本地主目录:

bash
TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson"

只有链接工作树之外的隐式开发运行,才使用共享的开发目录:

bash
TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson"

跟踪所选文件:

bash
tail -f "$TRACE_FILE"

显示失败的跨度:

bash
jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | {
  name,
  durationMs,
  exit,
  attributes
}' "$TRACE_FILE"

显示慢跨度:

bash
jq -c 'select(.durationMs > 1000) | {
  name,
  durationMs,
  traceId,
  spanId
}' "$TRACE_FILE"

检查内嵌的日志事件(event):

bash
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"

顺着一条追踪看下去:

bash
jq -r 'select(.traceId == "TRACE_ID_HERE") | [
  .name,
  .spanId,
  (.parentSpanId // "-"),
  .durationMs
] | @tsv' "$TRACE_FILE"

筛选编排命令(command):

bash
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 活动:

bash
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 中的推荐流程:

  1. 打开 Explore。
  2. 选择 Tempo 数据源。
  3. 把时间范围设成较近的一段时间,例如 Last 15 minutes。
  4. 先从宽处查起。不要一开始就用非常窄的查询。
  5. 先找来自 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_duration
  • t3_orchestration_command_duration
  • t3_provider_turn_duration(提供方(provider)适配器(adapter)启动一个回合(turn)要多久,不是这个回合自己的运行时长)
  • t3_git_command_duration

计数器告诉你数量和失败率:

  • t3_rpc_requests_total
  • t3_orchestration_commands_total
  • t3_provider_turns_total
  • t3_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 的追踪导出;直接到达环境的请求,永远不会并入发送方的追踪。

问题是下面这些时,用指标:

  • 「是不是一直都慢?」
  • 「改过之后是不是更糟了?」
  • 「哪种命令类型失败得最频繁?」

问题是下面这些时,用追踪:

  • 「这一次具体的请求里发生了什么?」
  • 「是哪个子跨度造成了这一次变慢的交互?」
  • 「失败的这条流程里打出了哪些日志?」

常见工作流 ​

「这次请求为什么失败了?」 ​

  1. 从本地 NDJSON 文件开始。
  2. 找出 effect-span 记录中满足 exit._tag != "Success" 的那些。
  3. 按 traceId 分组。
  4. 检查同级跨度(span)和跨度事件(event)。
  5. 如有需要,转到 Tempo 查看完整的追踪树。

「为什么界面感觉变慢了?」 ​

  1. 在追踪文件或 Tempo 里搜索较慢的顶层跨度。
  2. 检查子跨度中的 SQLite、Git、提供方(provider)或终端工作。
  3. 查看对应的耗时指标,判断这种变慢是不是系统性的。

「是 Git 钩子在造成延迟吗?」 ​

  1. 筛选 git.operation 跨度。
  2. 检查 git.hook.started 和 git.hook.finished 事件。
  3. 把钩子的耗时与包住它的 Git 跨度时长作比较。

「为什么本地有跨度,Grafana 里却什么都没有?」 ​

通常是下面某一条成立:

  • 没有设置 T3CODE_OTLP_TRACES_URL
  • 应用是从另一个环境启动的,不是你导出这些变量的那个环境
  • 更改环境变量之后没有完全重启应用
  • Grafana 查看的时间范围或服务名不对

如果本地 NDJSON 文件还在更新,说明本地追踪是正常的。问题几乎总是出在 OTLP 导出配置,或进程的启动方式上。

以后给代码加追踪时该怎么想 ​

优先划在边界上,而不是细碎的辅助函数 ​

合适的跨度(span)边界:

  • RPC 方法
  • 编排命令(command)的处理
  • 提供方(provider)适配器(adapter)调用
  • 外部进程调用
  • 持久化写入
  • 队列交接

不要给每一个细碎的辅助函数都打追踪。大多数辅助函数应当继承当前的活动跨度,而不是自己再开一个。

已经有 Effect.fn(...) 的地方就复用它 ​

代码库里已经大量使用 Effect.fn("name")。它通常应当作为你的第一道追踪边界。

临时要打的追踪:

ts
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、路径以及其他详细上下文,用跨度标注来放:

ts
yield *
  Effect.annotateCurrentSpan({
    "provider.thread_id": input.threadId,
    "provider.request_id": input.requestId,
    "git.cwd": input.cwd,
  });

让指标标签保持低基数 ​

合适的指标标签:

  • 操作类型
  • 方法名
  • 提供方类型
  • 聚合类型
  • 结果

不合适的指标标签:

  • 原始的对话线程(thread)ID
  • 命令 ID
  • 文件路径
  • cwd
  • 完整提示词
  • 已经有规范化的系列标签时,仍使用完整的模型字符串

详细上下文属于跨度,不属于指标。

把日志当作跨度事件 ​

打在跨度里的日志,会成为这条追踪的一部分:

ts
yield * Effect.logInfo("starting provider turn");
yield * Effect.logDebug("waiting for approval response");

因为安装了 Logger.tracerLogger,这些消息会以跨度事件(event)的形式出现。

使用可管道组合的指标 API ​

withMetrics(...) 是给一个效应挂上计数器和计时器的默认做法:

ts
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:单个文件的轮转大小,默认 10485760
  • T3CODE_TRACE_MAX_FILES:轮转文件的数量,默认 10
  • T3CODE_TRACE_BATCH_WINDOW_MS:刷盘窗口,默认 1000
  • T3CODE_TRACE_MIN_LEVEL:最低追踪级别,默认 Info
  • T3CODE_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:导出间隔,默认 10000
  • T3CODE_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。

bash
pid="$(jq .pid "${T3CODE_HOME:-$HOME/.t3}/userdata/server-runtime.json")"
ps -p "$pid" -o command=

如果 ps 显示的是 T3 Code 服务器,再发送信号:

bash
kill -USR2 "$pid"

文件为 <logsDir>/server-<pid>-<timestamp>.heapsnapshot,位于 server.trace.ndjson 旁边。要打开它,使用 Chrome DevTools 的内存(Memory)面板,然后选择加载(Load)。

拍一份之前:

  • 写入文件期间,服务器会停住。堆比较大时,可能要一分钟甚至更久。暂停期间,已连接的客户端(client)可以重新连上;如果这台服务器有事件循环监视器,会把这次暂停记成一次停顿。信号只发一次。写入过程中若再发一次信号,会在第一次写完之后再做一份快照。
  • 写入大约需要与堆占用相当的空闲内存。机器如果已经在换页,这样做可能让问题更严重,甚至让服务器崩溃。
  • 文件里是服务器内存中的全部内容,包括令牌、机密,以及对话线程(thread)的内容。不要公开分享。用完就删掉,因为存储清理不会移除它。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools