小程序实时日志

1 分钟阅读
·

小程序真机调试看不到日志、无法复现网络请求,我们复用实时协作文档的长连接基础设施,做了一套端上采集、长连上报、Kafka 缓冲、Elasticsearch 存储、面板检索的实时日志系统,巅峰期 300 台机器维持长连、日均日志量 1.2TB。

背景

小程序开发过程中,我们遇到一个很麻烦的问题:真机调试时看不到日志。

如果只需要支持微信小程序,这个问题还能靠 vConsole 缓解——它会在页面上挂一个悬浮的调试面板,把 console 输出和网络请求都展示出来。但当我们开始同时支持美团小程序后,情况变得更糟:美团小程序的双线程架构下,service 层(逻辑层)跑在独立的 JSCore 环境里,vConsole 这类挂载在 WebView 上的方案根本拿不到 service 层的日志,等于线上和真机的问题只能靠”猜”。

另外一个更痛的场景是网络请求排查。前后端联调时,我们经常需要把某一次真实请求的完整信息(URL、Header、Body、耗时)拿出来交给后端复现,但小程序的网络层是沙箱化的,开发者没有办法像浏览器一样直接在 DevTools 里导出请求。当时团队里流传的做法是让用户截图、口述现象,效率很低。

这两个问题指向同一个诉求:需要一套能够在生产环境下,实时把小程序端上发生的一切(日志、报错、网络请求)传回来,供研发查询定位的系统。

我在转到新零售业务之前,负责的是大象(美团内部 IM)实时协作文档的架构与研发,那套系统的核心正是一条稳定的长连接通道。日志上报本质上也是”客户端产生事件 → 服务端接收 → 持久化 → 可查询”,和协作文档的数据同步在传输层是同一件事。于是这次没有另起炉灶,而是直接复用了协作文档的长连接网关,只在两端做了针对性的适配:客户端侧接入日志采集 SDK,服务端侧新增日志的消费与存储链路。

整体设计

小程序实时日志系统整体架构

整条链路分四段:

  1. 小程序端:统一的日志模块拦截 console、网络请求、生命周期事件和未捕获异常,写入本地的环形缓冲池(ring buffer),再通过长连 SDK 异步上报。
  2. 长连接网关:复用协作文档已有的长连基础设施,负责连接管理、鉴权、协议解包,按小程序的 appId 做路由后转发给消息队列,网关本身不做业务逻辑。
  3. Kafka:作为上报链路和存储链路之间的缓冲层,削峰填谷,避免日志洪峰直接打到 Elasticsearch。
  4. Elasticsearch + 管理面板:消费 Kafka 后批量写入 ES,研发通过面板做检索、聚合、请求回放和异常订阅。

之所以要在端上先落一层环形缓冲池,是因为日志产生的时机和长连接建立的时机是不同步的:小程序冷启动阶段(onLaunch 到首屏渲染之间)往往是问题高发期,但这时候长连接可能还没建立完成。如果日志直接往连接上写,连接未就绪的这段时间产生的日志就会丢失,而这段恰恰是最需要排查的窗口。缓冲池按固定条数(而不是固定时间)做环形覆盖,保证内存占用可控,连接建立后按 seq 顺序补发,服务端如果发现 seq 不连续,就能感知到中间发生了丢弃,标记这条会话日志不完整。

日志包结构设计

把日志上报做成一个通用协议,核心是解决两个问题:

  • 公共部分怎么设计,才能让服务端和面板不需要理解每种业务日志的细节,就能做检索、聚合和会话串联;
  • 不同来源的日志(console、网络请求、生命周期)差异很大,怎么在一个协议里既统一又不互相牵制。

最后采用的方案是”公共 Meta + 按 type 分化的 body”:

日志上报包结构设计

公共 Meta 段包含 appIdversiondeviceId/openIdsessionIdtraceIdtimestamplevelpageseqsourcenetworkType 等字段,所有类型的日志都必须携带。这里面两个字段是解决前面提到的具体问题的关键:

  • source 区分日志来自 view 层还是 service 层,这正是美团小程序日志采集里最先要解决的问题——不区分来源的话,面板上看到一条 console.error 根本不知道它是在渲染层还是逻辑层抛出的,定位方向完全不同。
  • traceIdsessionId 用于把同一次用户操作或同一个网络请求前后产生的多条日志串起来,面板上点开一条错误日志,可以直接跳转到同一 traceId 下的完整时间线。

body 部分按 type 分成三类:

  • type: console:对应 console.log/warn/error,记录序列化后的参数数组,出错时附带堆栈。
  • type: network:记录请求方法、URL、Header、Body、响应状态和分阶段耗时(DNS、连接、首字节、总耗时)。这类日志的结构和 HAR (HTTP Archive) 格式基本对应,因此面板可以直接把它转换成标准 .har 文件,或者一键生成 curl 命令,解决了前面提到的联调场景。
  • type: lifecycle / custom:记录 onLaunchonShowonError 等生命周期事件,以及业务方自定义的埋点。

设计上还有两个容易被忽略但很重要的细节:

  • 截断与脱敏body 内容超过阈值直接截断并标记 truncated,避免个别异常参数(比如整段 HTML 或超大对象)把单条日志撑到几十 KB,拖慢整条链路;Authorization、身份证号等敏感字段在端上采集阶段就按规则替换,不允许明文进入后续任何一个环节。
  • 丢弃感知:前面提到的 seq 序号,不仅用于端上缓冲池覆盖时的顺序保证,服务端和面板也会用它检测某个时间段内是否发生过日志丢失,避免”日志没报错就等于没问题”的误判。

服务端存储与查询设计

日志从网关写入 Kafka 之后,走的是标准的消费-落库路径,但因为流量体量比较大,在几个环节上做了针对性的处理:

Kafka 写入链路与日志管理面板

Kafka 层:按 appId 哈希分区,避免单个小程序的流量热点打到同一个分区;副本因子设为 3 保证不丢消息;消息保留 3 天,即使下游消费或 ES 写入出现问题,也能在窗口期内重放补偿。写入组、告警组、采样组分别是独立的消费组,彼此互不影响、可以独立扩缩容——比如告警规则匹配逻辑变更需要重启消费者时,不会影响日志正常写入 ES。

Elasticsearch 层:索引按 mp-log-{appId}-{yyyy.MM.dd} 的模板创建,天然支持按小程序、按天做数据隔离和过期清理。考虑到 1.2TB/日的写入量,如果所有数据都放在同一批高配节点上,成本会很高,所以做了冷热分层:当日数据落在 Hot 节点,兼顾写入吞吐和查询延迟;超过一定天数后通过 ILM(Index Lifecycle Management)自动滚动到 Warm 节点,再往后直接按策略删除。这样可以把大部分机器资源集中在最常被查询的近期数据上。

日志管理面板:面向研发同学,主要提供几类能力:

  • 实时监控:按 appId、版本订阅,效果类似 tail -f,可以在灰度发布时盯着新版本的错误日志滚动;
  • 检索与聚合:支持按 traceIddeviceId、关键字检索,以及错误 Top N、接口耗时分布这类聚合视图,用来快速定位影响面最大的问题;
  • 请求回放:把 network 类型日志转成 curl 命令或导出 HAR 文件,直接解决最初提到的前后端联调痛点;
  • 会话串联:按 sessionId 把一次用户使用过程中的日志、报错、请求还原成一条时间线,而不是零散的日志行;
  • 异常告警:错误日志命中规则后推送到群消息,不需要研发主动登录面板才能发现问题;
  • 权限隔离:按小程序归属团队分配可见范围,避免日志权限扩散。

落地效果与规模

这套系统上线后逐步接入了公司内多个小程序业务,巅峰期同时维持长连接的机器规模大约 300 台,日均日志写入量约 1.2TB。这个体量已经不小,也验证了一开始”复用长连接基础设施而不是另起一套上报通道”的判断是合理的——协作文档场景本身就要求这条链路能承载大规模并发连接和持续的数据流,日志场景只是换了一种 payload。

真正上线之后,团队内部对小程序问题的排查方式发生了明显变化:过去依赖用户描述和截图,现在可以直接按 traceId 或用户设备在面板里拉出完整链路;过去后端联调只能靠口头对参数,现在可以直接把面板导出的 curl 甩过去复现。这也是这次做实时日志系统最初想解决的两个具体问题。

参考资料

  1. HAR (HTTP Archive) - Wikipedia
  2. HTTP Archive 1.2 Spec

331 字 · 48 段落
ximing

Written by ximingFollow onGitHub

相关文章