ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

iii 可观测性实战:Linkly 教程第 2 章——用内置 console 与 iii-observability 读取全链路日志和 Trace

2026/9/14 17:15:39 拓冰建站 浏览量
iii 可观测性实战:Linkly 教程第 2 章——用内置 console 与 iii-observability 读取全链路日志和 Trace iii 可观测性实战Linkly 教程第 2 章——用内置 console 与 iii-observability 读取全链路日志和 Trace【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii在 iii 引擎中可观测性不是事后挂载到各个服务上的组件所有跨 worker 的调用天然流经引擎因此引擎可以对整个系统做端到端的追踪与日志采集。本篇以 Linkly 教程的第 2 章Observe everything为主线完整讲解如何用iii console浏览器界面实时观察调用瀑布图再用iii trigger直接调用engine::logs::list、engine::traces::list、engine::traces::tree等内置函数从引擎中读取与界面完全相同的日志和 trace 数据。读完本篇你将掌握在不引入任何追踪库、不手动传递 request ID 的前提下定位一次 HTTP 跳转请求跨 worker 的完整执行路径并按耗时排序找出最慢的链路。前提第 1 章搭好的 Linkly 与自动注入的 iii-observability本篇假设你已完成 Linkly 教程第 1 章基础搭建自定义的linkworker 提供了link::create与link::resolve链接暂存在内存中并通过httpworker 暴露为POST /links和GET /s/:code两个端点引擎自第 1 章起一直处于运行状态。第 2 章的核心机制是引擎会自动注入iii-observabilityworker。正如教程原文所说你并没有添加任何追踪库也没有在服务之间手工串起 request ID——每个请求都会获得 trace每个Logger行都会被跨 worker 自动采集。因此iii-observability不应被声明在config.yaml或worker-compose.yaml中。这一点在仓库源码中得到了印证可观测性 worker 的 READMEengine/src/workers/observability/README.md开宗明义iii-observabilityis injected by the engine. Do not declare it inconfig.yaml,engine.workers, or projectcontainers:其运行时配置应通过configuration::set修改该 worker 的实现位于 engine/src/workers/observability/mod.rs它向引擎注册了完整的日志、traces、metrics、alerts、sampling 等查询函数族并内置log与trace两种 trigger 类型。iii-observability以OpenTelemetry协议输出 traces、metrics 和 logs因此你不会被绑定在 console 上可以把它指向 Honeycomb、Grafana、Datadog 或任何 OTel 兼容的后端。其运行时的全部配置项由 configuration worker 管理配置 id 为iii-observability。对于大多数团队日常使用 console或你自己的 OTel 后端就已经足够了。打开 console实时观看跨 worker 的 span 瀑布图启动 console——一个用于检查引擎的浏览器 UIiii console然后在 http://127.0.0.1:3113/traces 打开它。iii console是 CLI 内建的命令可在 engine/src/cli/registry.rs 中看到console命令的注册而3113 是 console UI 的默认端口对应 console/README.md 中-p, --port port选项默认值3113。页面上会列出你添加的每一个 worker 及其注册的全部 functions 和 triggers。切换到 traces 标签页然后制造一些流量观察调用实时流入curl -s -X POST http://127.0.0.1:3111/links \ -H Content-Type: application/json -d {url:https://iii.dev,code:iii} for n in $(seq 1 5); do curl -s -o /dev/null http://127.0.0.1:3111/s/iii; done点击任意一条 redirect可以看到一次跨越http进入link再返回的、带时间标注的完整 span 瀑布图直接读日志engine::logs::list如果你想绕过界面、直接从引擎读数据可以用iii trigger触发引擎内置函数。先制造一些流量包含一个不存在的链接制造found: false的日志curl -s -X POST http://127.0.0.1:3111/links \ -H Content-Type: application/json -d {url:https://iii.dev,code:iii} for n in $(seq 1 5); do curl -s -o /dev/null http://127.0.0.1:3111/s/iii; done curl -s -o /dev/null http://127.0.0.1:3111/s/missing然后查询日志iii trigger engine::logs::list limit100 \ | jq .logs[] | select(.body link resolved) | { body, data: (.attributes | with_entries(select(.key | IN(trace_id,span_id,service.name) | not))), trace_id, service_name }这条jq管道把响应过滤为link resolved条目并只保留本教程关心的字段去掉它可以看到 iii 引擎能提供的全部信息。典型输出{ body: link resolved, data: { log.data: { code: iii, found: true } }, trace_id: 797d427e4d0c3491cfc45f0d40c4e1b1, service_name: iii-node }这里有几个关键点data恰好就是你在 worker 中传给logger.info的结构化数据。引擎把这些字段存为独立的日志属性attributes所以上面的jq只是把所有 OTel 元数据键trace_id、span_id、service.name排除后把其余字段聚合回data。trace_id把这条日志和它所属的 trace 关联起来——这正是下一节要用的线索。从源码看engine::logs::list的入参定义在 engine/src/workers/observability/mod.rs 的LogsListInput结构中支持比教程用到的limit更丰富的过滤参数类型说明start_time/end_timeu64Unix 时间戳毫秒范围过滤trace_id/span_idstring按 trace 或 span 精确过滤日志severity_mini32最低严重级别1–24数值越大越严重severity_textstring按级别文本过滤如ERROR、WARN、INFOoffset/limitusize分页偏移与数量上限响应类型LogsListResult返回logs存储的 OTel 日志记录、total分页前的总匹配数以及时间戳。另外 engine/src/workers/observability/README.md 还列出了姊妹函数engine::logs::clear清空内存中的日志——在只读环境里你只需要用到list。沿一次跳转追踪跨 worker 的 span 树在 iii 系统中每一件事都有 trace。HTTP 请求的 trace 覆盖该请求的完整执行上下文。下面把最近一次 redirect 的trace_id捕获进 shell 变量然后把整个请求作为树展开trace_id$(iii trigger engine::traces::list nameGET /s/:code limit1 | jq -r .traces[0].trace_id) iii trigger engine::traces::tree trace_id$trace_id | jq -r def walk(depth): ( * depth // ) .name ( .service_name ) (((.end_time_unix_nano - .start_time_unix_nano) / 1e6 * 1000 | round) / 1000 | tostring) ms, (.children[]? | walk(depth 1)); .roots[] | walk(0) 这条jq递归遍历嵌套的roots树按深度缩进每个 span并打印其service_name与毫秒级耗时。你得到的是同一次 redirect 横跨两个 worker 的完整路径GET /s/:code (iii) 2.044 ms execute http::redirect (iii-node) 1.444 ms execute link::resolve (iii-node) 0.52 ms这棵树清晰展示了调用链请求经由httpworker 的 Trigger 从/s/:code进入调用link的http::redirect后者再通过引擎调用linkworker 的link::resolve。每个 span 的耗时直接告诉你请求把时间花在了哪一跳。两个源码层面的细节值得注意name过滤是大小写不敏感的子串匹配。engine::traces::list的nameGET /s/:code之所以能命中是因为实现中对代表根 span 的名称做了contains子串比较见trace_might_match_root_filtersengine/src/workers/observability/mod.rs。Worker span 的导出存在短暂延迟。教程原文特别提示worker span 是在稍后才批量导出的因此刚发出的新请求的 trace 可能缺失或看起来被截断稍等一两秒再试即可。这与源码中 OTel 的批处理导出机制相符——引擎侧 span 直接落内存存储而 workerSDK侧 span 经 OTLP 批量送达因此看到完整树需要一点缓冲时间。对比 trace按耗时排序找出最慢的跳转要横向比较多条 trace可以在一次调用里完成过滤、列出和排序。下面是把 redirect trace 按耗时降序排列最慢在前iii trigger engine::traces::list nameGET /s/:code sort_byduration_ms sort_orderdesc limit10 \ | jq -r .traces[] | select(.end_time_unix_nano ! null) | \(((.end_time_unix_nano - .start_time_unix_nano) / 1e6 * 1000 | round) / 1000) ms \(.trace_id)每一行把一个耗时和它的trace_id配对最慢的在最上面2.044 ms 6b20e1fe001742c25bb7dc570b57fe42 1.700 ms 797d427e4d0c3491cfc45f0d40c4e1b1最慢的跳转浮到顶端后拿任意一条的trace_id喂给engine::traces::tree就能定位是哪一跳出了问题。从源码结构看engine::traces::list的实际能力远不止教程用到的这三个参数。TracesListInputengine/src/workers/observability/mod.rs完整支持参数说明trace_id/trace_ids按单个或一组 trace ID 过滤offset/limit分页limit默认 100service_name按服务名过滤大小写不敏感的子串匹配name按 span 名过滤大小写不敏感的子串匹配status按状态过滤error、pending、ok、unset大小写不敏感min_duration_ms/max_duration_ms毫秒级亚毫秒精度耗时范围过滤start_time/end_timeUnix 毫秒时间范围匹配与之重叠的 spansort_by排序字段start_time|duration别名duration_ms|service_name|name默认start_timesort_order排序方向asc|desc默认ascattributes/exclude_attributes按 span 属性键值对做 AND / 排除过滤精确匹配include_internal是否包含引擎内部 traceengine.*函数默认falsesearch_all_spans为true时只要 trace 中任意span 匹配name过滤即命中默认只匹配根 spanattribute_projection只在每条 trace 摘要上投影指定的属性键完整属性仍保留在spans与tree中每条 trace 摘要TraceSummary还聚合了span_count、error_count、function_id、topic、trace_tags等字段——状态判定逻辑是任一 span 为 error 或iii.tag.outcome为failed/error记为Error存在未结束的 pending span 记为Pending否则为Ok。这正是 console 界面上 traces 列表按耗时排序、带状态着色背后的同一份数据。另外源码中的memory_exporter_not_enabled_error提示了一个适用前提这些查询函数依赖内存 span 存储若配置中未启用memory或bothexporter会返回In-memory span storage is not available. Set exporter: memory or both in config.——本地开发默认走内存存储而生产环境若要同时导出 OTLP 并保留 iii 内查询应使用exporter: both组合详见 engine/src/workers/observability/README.md 的 OTLP Transport 一节。延伸阅读engine::traces::* 家族与配套能力教程用到的三个函数只是可观测性 API 的子集。engine/src/workers/observability/README.md 完整列出了同一 worker 提供的函数族方便在 Linkly 后续章节中按需取用函数用途engine::logs::list/engine::logs::clear查询 / 清空存储的日志条目engine::traces::list每条 trace 返回一条紧凑摘要支持上述全部过滤、排序与分页engine::traces::spans列出完整的 span 记录含 attributes、events、linksengine::traces::tree把一条 trace 还原为层级 span 树engine::traces::clear清空内存中的全部 spanengine::metrics::list/engine::rollups::list引擎计数器调用量、worker 生灭、性能分位数与 SDK 指标、时间窗口聚合engine::log::{info,warn,error,debug,trace}在任意 worker 中写结构化日志接受message、data、trace_id、span_id、service_nameengine::baggage::get/set/get_all读写当前 trace 上下文中的 baggageengine::sampling::rules、engine::health::check、engine::alerts::list/evaluate采样规则、引擎健康状态、告警规则查询与手动求值配套的还有两种trigger 类型可以把观测数据变成事件驱动的反应logtrigger 按级别info/warn/error/debug/trace缺省为全部订阅日志条目tracetrigger 则是一个合并后的traces 变化心跳约 300ms 防抖处理函数收到窗口内受影响的 trace id 列表后再用engine::traces::*重读详情——console 界面上新调用实时流入的 reactive 刷新正是这一机制而非轮询。小结与下一步至此Linkly 变得完全可观测console 实时展示每一个 worker、trace 和日志而你同样可以用iii trigger从引擎直接读取这些数据的任何切面——按trace_id把日志和 trace 缝合用 span 树定位跨 worker 的慢跳用sort_byduration_ms批量对比找出异常。需要记住的限制是链接目前只存在内存中重启引擎就会清空。下一章 Ch. 3: Persist everything 将引入databaseworkerSQLite把链接和点击事件移入持久化存储。【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考