news 2026/9/14 13:35:58

iii 全链路可观测性实战:用控制台与 engine 级日志/追踪观测 Linkly 短链服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
iii 全链路可观测性实战:用控制台与 engine 级日志/追踪观测 Linkly 短链服务

iii 全链路可观测性实战:用控制台与 engine 级日志/追踪观测 Linkly 短链服务

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

本教程基于 Linkly 短链服务(iii 项目的官方实战教程第二章),讲解 iii 系统的端到端可观测性:无需为每个服务单独接入 tracing 库,只要跨 worker 调用流经引擎,引擎就能自动为整个系统生成分布式追踪与结构化日志。读完本章,你将掌握三件事:用iii console实时查看 worker、trace 与日志;为函数补充结构化日志并通过engine::logs::list直接读取;用engine::traces::list/engine::traces::tree还原一次跨 worker 请求的完整调用树与耗时分布,从而定位"哪个环节拖慢了请求"。

为什么 iii 的可观测性是"系统属性"而非"服务插件"

在传统微服务架构里,端到端追踪通常意味着在每个服务中引入 tracing 库、手动传递 request ID、统一上报格式,才能把散落的日志串成一条链路。iii 的不同之处在于:每一次跨 worker 调用(worker.trigger)都必然经过引擎,因此引擎天然拥有整张调用图,可以替整个系统完成 tracing 与日志采集,而不需要每个 worker 各自接入 SDK 或透传上下文。

这一点在 Linkly 教程里体现得非常直接:iii project init创建项目时,默认的config.yaml中已包含iii-observabilityworker(引擎内置注入,无需也不应在config.yaml中重复声明,详见 engine/src/workers/observability/README.md)。此后:

  • 每个请求都会自动获得一条 trace;
  • 每个Logger输出(例如logger.info(...))都会被自动采集并关联到对应 trace;
  • 所有 worker 的日志与 span 统一汇入同一个存储与查询面。

也就是说,可观测性不是"装在每个服务上的附加件",而是系统本身固有的属性。这也正是本章想让你停下来体会的核心观点。

打开控制台:观察 Linkly 实时运行

前置条件是引擎仍在运行(第一章启动的引擎不要关)。另开一个终端启动控制台——一个用于检视引擎的浏览器 UI:

iii console

在浏览器打开 http://127.0.0.1:3113。控制台会列出你添加的每一个 worker,以及它们注册的函数(functions)与触发器(triggers)。左侧导航栏的 WORKERS、FUNCTIONS、TRIGGERS、STATES、QUEUES、TRACES、LOGS、CONFIG 等面板分别对应引擎的不同可观测面。

现在制造一点流量,让调用在界面上"实时流进来":先创建一个短链,再连续访问它 5 次:

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

回到控制台的 TRACES 面板,你会看到GET /s/:code的追踪条目持续刷新。点击任意一条 redirect 追踪,右侧会展开完整的瀑布视图(waterfall),展示一次跳转如何从iii-http进入linkworker、再经引擎折返的完整 span 时间线:

注意:为了得到这些追踪,你没有添加任何 tracing 库,也没有在服务之间手动传递 request ID。这正是前文"系统属性"的直观印证。

可观测性的底座:iii-observability 与 OpenTelemetry

控制台只是iii-observabilityworker 提供的一个展示面。从源码与内置文档看(engine/src/workers/observability/skills/SKILL.md),该 worker 基于OpenTelemetry(OTel)构建:分布式追踪、结构化日志、带 rollup 的指标、告警规则、采样配置、baggage 传播全部开箱即用,且每个能力面都是一个可调用的engine::*函数

这意味着你不会被控制台锁死:

  • 其 traces、metrics、logs 都以 OTel 格式发射;
  • 把该 worker 指向任意 OTel 兼容后端(如 Honeycomb、Grafana、Datadog 等),iii 的追踪数据即可直接流入;
  • 若想保留控制台的本地查询能力、同时外发到 collector,可将 exporter 设为both(trace 与 log 分别对应exporterlogs_exporter两个字段)。

关键的配置项(可通过configuration::set以 id 为iii-observability写入运行时配置,或直接使用对应的OTEL_*环境变量):

字段类型/默认值说明
enabledboolean,默认falseOTEL_ENABLED是否启用 OpenTelemetry 追踪导出
service_namestring,默认"iii"OTEL_SERVICE_NAMEtrace/metrics 中的服务名,即前文日志里的service_name
exportermemory/otlp/both,默认otlpOTEL_EXPORTER_TYPEtrace 导出方式;本地开发常用memory,生产外发可用both
endpointstring,默认http://localhost:4317OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector 地址;https://走 TLS,http://走明文
sampling_ratio0.01.0,默认1.0OTEL_TRACES_SAMPLER_ARG全局 trace 采样比例
memory_max_spansnumber,默认1000OTEL_MEMORY_MAX_SPANS内存中保留的最大 span 数
logs_enabledboolean是否启用结构化日志存储
logs_exportermemory/otlp/both,默认memoryOTEL_LOGS_EXPORTER日志导出方式
logs_max_count/logs_retention_secondsnumber,默认1000/3600内存日志条数上限与保留时长
leveltrace/debug/info/warn/error,默认info存储的最低日志级别
alertsAlertRule[]基于指标的告警规则(如iii.invocations.error超过阈值触发)

OTLP 传输默认走 gRPC;如需 OTLP/HTTP protobuf,可在启动引擎前设置OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf(或用OTEL_EXPORTER_OTLP_TRACES_PROTOCOL/OTEL_EXPORTER_OTLP_METRICS_PROTOCOL做信号级覆盖),iii 会自动为 traces/metrics 追加/v1/traces/v1/metrics路径,日志则发往/v1/logs。认证头可通过OTEL_EXPORTER_OTLP_HEADERS等环境变量注入,凭证应放在环境变量或密钥管理器中,不要写进配置文件。

对大多数团队而言,控制台(或你自己的 OTel 后端)已经覆盖日常需求。本章剩余部分是一个可选深挖:直接从引擎读取同样的日志与追踪。这正是把可观测性接入脚本、CI 或 Agent 的方式。如果只想走常规路径,可以直接跳去 Ch. 3: Persist everything 学习持久化。

link::resolve补上日志

第一章里link::create已经写入了日志(logger.info("link created", { code, url }))。为了让每一次短码解析也有据可查,给link::resolve加一行对应的结构化日志。编辑link/src/index.ts

worker.registerFunction("link::resolve", async (payload: { code: string }) => { const stored = await worker.trigger<{ scope: string; key: string }, { url: string } | null>({ function_id: "state::get", payload: { scope: "links", key: payload.code }, }); logger.info("link resolved", { code: payload.code, found: !!stored?.url }); return { url: stored?.url ?? null }; });

关键点在于logger.info("link resolved", { code: payload.code, found: !!stored?.url }):第二个参数是一个结构化对象,它会原样进入日志的log.data字段(稍后你会从引擎读回它)。由于 worker 以tsx watch方式运行(见第一章的package.json),保存文件后 worker 会自动重载,新日志即刻生效。

保存后制造一些混合流量——包含一次成功解析、5 次重复访问、以及一次必然 404 的未知短码:

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除了调用业务函数,还可以调用引擎内置的遥测函数。读取最近 100 条日志,并用jq只筛出link resolved条目、投影出我们关心的字段:

iii trigger engine::logs::list limit=100 \ | jq '.logs[] | select(.body == "link resolved") | { body, "log.data": .attributes["log.data"], trace_id, service_name }'

这里的jq管道把响应裁剪到link resolved条目,只保留本教程关心的字段;去掉管道可以看引擎能提供的全部信息。

输出类似:

{ "body": "link resolved", "log.data": { "code": "iii", "found": true }, "trace_id": "6b20e1fe001742c25bb7dc570b57fe42", "service_name": "iii-node" }

逐字段解读:

  • log.data与你传给logger.info的对象完全一致——这是结构化日志的价值:机器可读、可过滤、可聚合;
  • trace_id把这条日志关联回它所属的 trace,这正是下一步要追查的线索;
  • service_nameiii-node,说明这条日志由 Node SDK 侧的 worker 记录。

engine::logs::list支持丰富的过滤参数,对应实现见 engine/src/trigger_formats.rs 中engine::traces::list/engine::traces::tree/engine::logs::list的函数注册;日志查询面还支持start_time/end_time/trace_id/span_id/severity_min/severity_text/offset/limit等过滤器(详见 engine/src/workers/observability/skills/SKILL.md 的函数表)。此外,iii-observability还提供engine::log::info等五个发射函数(info/warn/error/debug/trace),以及engine::logs::clear用于清空内存日志存储。

追踪一次跨 worker 的跳转

每个请求同时也是一条 trace。先取最近一条 redirect 追踪:

iii trigger engine::traces::list name="GET /s/:code" limit=1

拿到结果中的trace_id后,用engine::traces::tree把整条请求还原成一棵父子 span 树。下面的jq脚本会按深度缩进每个 span,并打印其service_name与耗时(毫秒):

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树,按深度缩进并打印service_name与毫秒级耗时。一次跳转的完整路径横跨两个 worker,输出形如:

GET /s/:code (iii) 2.32 ms call http::redirect (iii) 2.228 ms call http::redirect (iii-node) 1.335 ms handle_invocation link::resolve (iii) 0.624 ms call link::resolve (iii) 0.582 ms call link::resolve (iii-node) 0.174 ms

读这棵树,你能看清整条链路:

  1. 请求经iii-http到达,产生根 spanGET /s/:code(引擎侧,服务名iii);
  2. 引擎调用linkworker 上的http::redirect,span 在引擎侧(iii)与 worker 侧(iii-node)各记录一次;
  3. http::redirect内部再经引擎调用link::resolve,同样两侧各有一个 span,直至iii-state返回结果。

每个 span 的耗时都单独计时——这就是请求慢下来时你该去看的视图:通过逐级耗时对比,能立刻判断瓶颈在引擎调度、worker 处理还是某个下游函数。注意这里 worker 侧的 span 通过 OTLP 摄取,会有短暂的导出延迟,因此刚发起的请求 trace 可能在最初一两秒看起来不完整;稍等片刻或改读稍早的 trace 即可。

engine::traces::list的查询参数

从源码看(engine/src/workers/observability/mod.rs 中的TracesListInput),engine::traces::list支持以下过滤器与分页参数,非常适合接入脚本或 Agent:

  • trace_id:按指定 trace ID 精确过滤;trace_ids支持一次展开一组 trace;
  • service_name/name:按服务名或 span 名做大小写不敏感的子串匹配;
  • statuserror/pending/ok/unset
  • min_duration_ms/max_duration_ms:按 span 时长(毫秒,支持亚毫秒精度)过滤,这正是"找慢请求"的基础;
  • start_time/end_time:Unix 毫秒时间窗过滤;
  • sort_bystart_time/duration(别名duration_ms)/service_name/name,默认start_time)与sort_orderasc/desc,默认asc);
  • search_all_spans:为 true 时按 trace 内任意span 匹配 name 过滤,而非仅匹配根 span;
  • attribute_projection:只返回指定任意属性,缩小响应体;
  • offset/limit(默认 100)用于分页;
  • include_internal:是否包含引擎内部(engine.*)span,默认排除。

配套的engine::traces::spans返回完整 span 记录(含 attributes、events、links),适合需要完整载荷的细节/时间线消费者;engine::traces::tree则以trace_id为必填参数返回层级树;另有engine::traces::group_by可按属性聚合 span 统计、engine::traces::clear清空内存 span 存储。

找出最慢的短链

单条 trace 只能看一次请求;要横向对比大量请求,就按耗时把 redirect span 排序,最慢的排最前:

iii trigger engine::traces::list name="GET /s/:code" sort_by=duration_ms sort_order=desc limit=10 | jq -c '[.spans[] | ((.end_time_unix_nano - .start_time_unix_nano) / 1e6 * 1000 | round / 1000)]'

最慢的 redirect 会浮到列表顶部;拿到任意一条的trace_id后用engine::traces::tree展开,就能定位是哪一个 hop(引擎调度、worker 处理、下游函数)在拖后腿。这正是把"用户感觉慢"转化为"具体 span 慢"的标准排查路径。

注:当前仓库文档中标注了一个验证遗留问题——sort_by=duration_ms的排序在引擎某次修复落地前可能出现倒序/未排序的情况;若排序结果与预期不符,可先手动比对 span 时长,待引擎修复后此行为将恢复正常。

小结与下一步

至此,Linkly 已经"可观测"了:

  • 控制台(iii console)实时展示每一个 worker、trace 与 log;
  • iii trigger engine::logs::list可读取同样的结构化日志,trace_id将日志与 trace 关联;
  • engine::traces::tree可把一次跨两个 worker 的跳转还原成带逐级耗时的 span 树;
  • engine::traces::list的排序与时长过滤能力可横向对比、找出最慢请求。

你全程没有引入任何 tracing 库或手动透传 request ID——端到端可观测性是 iii 系统的固有属性,由引擎内置的iii-observabilityworker(OTel 底座)自动提供,且同一份数据既可查于控制台、也可经engine::*函数接入脚本/CI/Agent,还能以标准 OTel 协议外发到任意兼容后端。

目前的链接仍只保存在内存中(第一章把iii-state设成了in_memory),重启引擎即被清空。下一步进入 Ch. 3: Persist everything,把短链数据迁入持久化存储。

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 13:35:39

153、MLIR的Stream(流)编程模型与FIFO通信

MLIR的Stream(流)编程模型与FIFO通信 一个让我熬夜三天的bug 去年做AI加速器编译器的时候,遇到一个诡异的死锁问题。硬件仿真跑得好好的,一到FPGA上板就卡死,波形抓出来一看,两个加速器核之间的FIFO读指针永远停在0x3F,写指针却已经跳到0x80——溢出了。查了三天,最后…

作者头像 李华
网站建设 2026/9/14 13:34:36

手部几何识别实战:基于Matlab的图像预处理与BP神经网络实现

简介&#xff1a;基于Matlab实现的手部几何特征识别系统&#xff0c;面向生物识别、人机交互与无障碍技术开发者&#xff0c;提供从图像捕获、预处理、特征提取到匹配识别的完整算法流程&#xff0c;可作为课程设计或科研入门参考。压缩包共30个文件&#xff0c;约1.25MB&#…

作者头像 李华
网站建设 2026/9/14 13:32:38

SNMP协议栈选型:Net-SNMP与国产自研SDK的信创适配之道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 13:31:30

合并两个有序数组:从暴力排序到双指针原地归并的保姆级教程

合并两个有序数组这道题&#xff0c;在 LeetCode 上挂着 Easy 的标签&#xff0c;但真到了面试现场&#xff0c;它能淘汰的人远比想象中多。我印象很深&#xff0c;有一次候选人把“先合并再排序”写出来&#xff0c;然后理直气壮说这就是最优解&#xff0c;我追问了一句“那如…

作者头像 李华
网站建设 2026/9/14 13:27:03

MATLAB滑动窗口技术:高效数据预处理与特征提取

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华