Spring Insight 里如何对 Span 进行清洗

Spring Insight 里如何对 Span 进行清洗

作者:苏渡苇

项目地址github.com/iweidujiang...(感谢 Star !)

本文要讲什么

上一篇把存储讲完了:saveTraceSpans 负责 snapshot、淘汰、可选刷盘,控制台查的也是那份内存列表。

数据并不是 POST 进来就往列表里扔,Controller 先交给 TraceSpanCollectorService,校验过了再清洗、补全,最后才调用存储。

Agent 这边字段有时会空着------服务名只写在批次信封上、耗时没算、statusCode 没填------直接入库的话,后面按服务聚合、按状态筛选都会对不上。

这篇要讲的就是门口这一步:一批 Span 怎样被判定有效,缺的字段怎样补上,以及为什么整批失败和单条跳过不是一回事。

一、先把问题说清楚

上报体对应的是 CollectorRequest,和 Agent 里的 TraceBatchReport 字段对齐:serviceNameserviceInstancebatchIdspans

HttpInsightBatchSink 组包时,服务名写在批次这一层,每条 Span 里也可能再带一份;Server 不能假定两条都有,更不能假定 durationMsspanKindstatusCode 都填好了。

所以收集服务要干三件事:这批能不能收、每条缺什么补什么、补完再交给上一篇那个存储。

代码在此:io.github.iweidujiang.springinsight.collector.service.TraceSpanCollectorService

二、整批先过一道门

processBatchRequest 一进来就问 request.isValid(),不通过就返回错误,Controller 映射成 400,列表里不会出现这批数据。

isValid() 看的是硬条件:spans 不能空,每一条都得有 traceIdspanId,缺一个整批退回。

java 复制代码
public boolean isValid() {
    if (spans == null || spans.isEmpty()) {
        return false;
    }
    for (int i = 0; i < spans.size(); i++) {
        TraceSpan span = spans.get(i);
        if (span.getTraceId() == null || span.getTraceId().trim().isEmpty()) {
            return false;
        }
        if (span.getSpanId() == null || span.getSpanId().trim().isEmpty()) {
            return false;
        }
    }
    return true;
}

这和存储里 缺 ID 就 skip看起来像重复,层级却不一样:收集服务面对的是一次 HTTP 批次,宁可整批发回来,也不让半残数据进清洗;存储那层是入库兜底,防止还有别的入口把脏对象塞进来。

Controller 上还有 @ValidserviceNameserviceInstance@NotBlankspans@NotNull,框架校验比 isValid() 更早,信封都缺的请求到不了收集服务。

过了这道门才会去改计数器、开 StopWatch、进入清洗,成功则 202,处理异常则 500。batchId 只进日志摘要,Server 目前没有按它去重,同一批重发一次就会再存一遍。

三、缺的字段就地补上

清洗在 cleanAndEnrichSpans 里,按条处理:Span 上没有服务名或实例名,就用批次信封上的顶上,然后再走 cleanAndEnrichSingleSpan

单条补全大致是这几件事:

spanKind 空着就写成 INTERNAL HTTP 进站一般是 SERVER,Feign 出站是 CLIENT,补不上的先给个内部类型,后面按 kind 筛选时至少不是 null

statusCode 空着就猜。 先看 tags 里的 http.status_code,2xx 写成 OK;没有 HTTP 码再看 success,为 true 也写成 OK;两条都没有,落成 ERROR。字段不全会被标成失败,这是当前实现,不是业务真的抛了异常。

durationMs 空着就减一下。 同时有 startTimeendTime 时,用结束减开始填进去,拓扑平均耗时、链路列表的 duration 都靠这个数。

tags 保证不是 null,再挂两个收集器自己的标签: collector.process.timecollector.version。后者现在写死成 1.0,跟项目版本号不是一回事,当处理痕迹看就行。

java 复制代码
if (span.getDurationMs() == null && span.getStartTime() != null && span.getEndTime() != null) {
    span.setDurationMs(span.getEndTime() - span.getStartTime());
}
if (span.getTags() == null) {
    span.setTags(new HashMap<>());
}
span.getTags().put("collector.process.time", Instant.now().toString());
span.getTags().put("collector.version", "1.0");

这些改动打在请求体里的原对象上,上一篇说的 snapshot() 发生在这之后,所以内存里留下的是补全后的拷贝,不是 Agent 刚发过来的那份引用。

单条清洗若抛异常,这条跳过、其余继续,和 isValid() 的整批失败是两条路:ID 这种硬字段整批拦,补全时的意外只丢掉出事的那一条。

四、和「入库时再判断」的区别

可以对照着看:

收集服务 存储
看到的是 一次 HTTP 批次 已经洗过的列表
traceId / spanId 整批 400 单条 skip
缺服务名、耗时、状态 补上再往下走 不再补,按现有字段存
失败时 可以整批拒绝 只决定进不进列表

收集放在存储前面,是因为 这批像不像监测数据列表怎么淘汰 」不是同一个问题。补字段、打 collector.* 标签、记成功失败条数,都不该塞进带锁的 ArrayList 里去做。

存储只认补完的结果:有 ID 就 snapshot 进去,超上限就挤旧的。两边各管一段,后面换文件还是换库,清洗规则不用跟着搬。

五、自己写收集入口时可以用到的

  1. 信封和明细分开补。 批次上有服务名,Span 上也可能有;明细空了用信封,明细有了别覆盖,跨服务一批里混着不同 serviceName 时才不会全被改成上报进程的名字。
  2. 硬校验和软补全分开。 没有 traceId 的批次没有清洗的意义;耗时没算、状态没填,可以猜,不该把整批打回。
  3. 猜状态要有默认值,也要知道默认值很凶。 现在缺 statusCode、又没有 HTTP 码、success 也不是 true,就会落成 ERROR,控制台错误分析会被这些「空字段」抬高。
  4. 先改原对象,入库再拷贝。 补全和 snapshot 分两步,收集侧不用自己 new 一份,存储侧也不用知道字段是谁填的。
  5. 计数器和写入分开。 AtomicLong 记收到、成功、失败,和列表内容不是同一件事;列表被挤掉旧数据之后,计数不会回滚。

六、小结

  • POST 进来之后先过 CollectorRequest.isValid()spans 为空或任何一条缺 traceId / spanId,整批不会入库。
  • 过门之后 cleanAndEnrichSpans 用批次上的服务名补空字段,再补 spanKindstatusCodedurationMs 和 tags。
  • 单条清洗出错只跳过这一条;存储里缺 ID 再 skip 一次,属于入库兜底。
  • 收集服务改的是请求里的原对象,snapshot() 在这之后,控制台看到的是补全后的拷贝。

数据放下之后,控制台还得把「谁调用了谁」画成图,边不是前端编的,是 Span 上的 remoteService 聚出来的。

下一篇预告:看拓扑的边是怎么从一批 CLIENT Span 里抠出来的,为什么只有 Feign 出站这类带远端服务名的记录才会变成箭头。

最后:欢迎围观 Spring Insight

如果你对轻量监测、Spring Boot Starter、链路埋点感兴趣,欢迎看看这个还在打磨的小项目:

🔗 GitHubgithub.com/iweidujiang...

当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。

你的 Star 是对我最大的鼓励。

相关推荐
谢亮_vipxieliang1 小时前
ValidX时间段验证详解:ISO 8601标准与简化格式
java·spring boot·后端·spring·spring cloud·hibernate
Bs_MoneyMagnet2 小时前
基于springboot+vue的爱心众筹系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·管理系统
EatFan3 小时前
从单体到模块化:我的 Spring Boot 项目为什么拆成 framework、module、server?
java·spring boot·后端
Bs_MoneyMagnet4 小时前
基于springboot+vue的在线音乐管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
仍然.4 小时前
SpringCloud---Seata
spring boot·后端·spring cloud
凤山老林4 小时前
Spring Boot + OpenSearch 实战:搞定全文检索、向量召回与混合排序
spring boot·后端·全文检索·向量·opensearch·全文索引
梅梅绵绵冰4 小时前
SpringCloud网关
java·spring·spring cloud
QQ_21696290965 小时前
基于微服务架构的店铺管理系统的设计与实现
大数据·spring boot·后端·spring·微服务·小程序·架构
EatFan18 小时前
Java接入支付宝 JSAPI 支付保姆教程(二):流程讲解与前后端代码讲解
前端·spring boot·后端·微信小程序·小程序·uni-app