实现原理、工程边界与接手开发指南
BAOFOO · OBSERVABILITY ENGINEERING

把业务日志接上链路,
把采样控制放进运行过程。

从 Agent 扩展到 Collector 补丁,沿着真实调用路径理解这份二次开发。结论以当前文件实现为准,同时保留 README 的交付主线。

Java Agent 2.30.0Collector 0.157.017 个阅读章节41 个源码入口
云端阅读版:源码索引会跳转到本文对应的说明位置,已摘录的代码可直接展开。完整源码仍保存在本地工程目录。

01 · 这份二开解决了什么

这套方案围绕两个实际问题展开:让技术链路与业务日志、商户、订单关联起来;让采样策略能够随排障和流量压力调整。 Java 侧通过 Agent 扩展在运行时补充字段,Collector 侧通过本地补丁接入 Apollo,最后将遥测数据写入 Doris。

阅读依据:根目录 README、Java 扩展源码、Collector 工作副本与补丁、构建脚本及部署配置。本文是截至 2026-10-12 的本地源码分析;没有执行构建、启动服务或验证生产运行效果。文中的“生产配置”指仓库内命名为 prod-bx 的文件,不代表线上当前配置。
关注点当前代码的做法实际价值
业务关联给当前 Span 补充 logId、memberId、memberTransId、errorCode;将部分字段桥接到 MDC。可以从日志 ID 找到相关 Span,再利用 traceId 查询链路;业务字段支持筛选与采样。
框架兼容Alibaba Dubbo 2.6 补建链路;Apache Dubbo 在已有 Span 上补业务字段。兼容历史框架,减少应用逐个改代码的工作。
采样控制Collector 尾部采样热更新 + Agent 应急头部采样。正常情况下按错误、慢请求、比例保留;应急时更早控制产生的数据量。
交付官方 Java Agent + 自定义扩展;OCB 组装精简 Collector。Agent 与 Collector 的二开边界分开,便于升级与追踪改动。
源码:README.md · 第 1 行起

02 · 架构与一条请求的经过

业务应用 JVM官方 Java Agent
Dubbo / MonitorLog 扩展
logId / MDC / Marker 扩展
↓ OTLP:Traces · Logs · Metrics
otelcol-slim限流保护 → 业务分流 → 过滤
Trace:尾部采样 → 批处理
↓ Doris Exporter
Doris主业务与 RM 两组库
链路、日志、指标分别写表
Apollo 是配置控制通道:① Agent 拉取应急采样比例;② Collector 轮询尾部采样策略;③ apollo: Provider 在加载配置时读取整份配置。三条通道的触发时机与能力不同。
  1. 请求进入应用后,官方埋点或 Alibaba Dubbo 扩展建立 Span,并维持当前 Context。
  2. 调用 Dubbo 时,扩展从参数对象中找 memberId、memberTransId;调用结束后从返回对象提取结果码。
  3. 应用读取或设置业务 logId 时,Advice 将值写入当前 Span,并尝试放入 MDC;日志输出前还会主动拉取已识别的 Holder。
  4. Logback 官方采集读取 MDC / Marker 等内容生成日志记录。Trace 与 Log 独立上报,是否保留 Trace 不自动决定是否保留 Log。
  5. Collector 先按命名空间等条件分流、过滤噪音,再对 Trace 等待并做尾部采样,最后批量写入对应 Doris 库。

关联的边界:代码提供“将业务 ID 写入遥测数据”的能力,完整检索还需要存储字段、查询语句和界面配合。本目录不能单独证明“任意日志 ID 都一定能查到完整链路”;采样、上下文丢失、Holder 未识别都会影响结果。

源码:baofoo-otel/config/otel-collector-prod-bx.yaml · 第 370 行起

03 · 关键技术与 ID 关联原理

这套二次开发主要用到了 Java Agent 字节码增强、反射、SPI、OpenTelemetry 上下文、MDC,以及 Apollo 配置轮询。理解它时,可以先追问:业务 ID 从哪里来,写到哪里,怎样与技术链路对应起来?

1. 具体关联了哪些 ID?

ID表示什么这份代码如何处理
traceId一次分布式请求的链路 ID。由 OpenTelemetry 维护,通过上下文传播机制跨服务传递。
spanId链路中某一次操作或调用的 ID。由 OpenTelemetry 维护,Span 自身携带 traceId 和 spanId。
logId业务系统原来的日志 ID。从业务 Holder 的方法或 MDC 中取得,写入当前 Span 的属性。
memberId商户 ID。从 Dubbo 参数或 MonitorLog 对象提取,写到当前 Span。
memberTransId商户交易订单 ID。按明确的字段名 / 别名提取,写到当前 Span;并非任意订单字段都能自动识别。

例如原来的业务日志只有“logId=LOG-888,订单处理失败”。扩展采集后,对应 Span 可以同时包含以下信息。这里的 ID 是便于阅读的示意值,真实 traceId / spanId 使用规定长度的十六进制格式。

Span 自身的链路标识:
    traceId = TRACE-123
    spanId  = SPAN-456

扩展写入的附加属性:
    logId         = LOG-888
    memberId      = M100
    memberTransId = ORDER-999
已知业务日志 ID:LOG-888从日志文件或问题单获得
↓ 查询具有该 logId 属性的 Span
找到 Span,取得 traceId业务字段与技术链路标识记录在同一条 Span 数据中
↓ 根据 traceId 查询链路
查看跨服务调用关系再结合日志中的 traceId 或业务 ID 查询相关日志
这里没有把 logId 改成 traceId,而是让它们记录在同一个 Span 上。上面的查询过程是利用这些字段可以实现的检索方式;扩展本身没有同时实现完整查询页面。字段成功采集、链路保留和存储查询支持都是前提。

2. 不修改业务源码,如何取到 logId?——Java Agent + Byte Buddy

业务原来执行 headerEntity.getLogId()。Agent 匹配到这个方法后,通过 Byte Buddy 的 Advice 在方法返回时插入额外处理。下面是概念伪代码,用来说明执行时序,并非替换业务方法的实际源码。

String logId = 原来的 getLogId();

// Agent 在运行时自动加入的处理
把 logId 写到当前 Span;
把 logId 放入 MDC;

return logId;
  • OnMethodEnter:方法进入前执行,例如拦截 setLogId 的传入值。
  • OnMethodExit:方法返回时执行,例如取得 getLogId 的返回值。
  • 业务团队不用为这段采集逻辑逐个修改应用源码;但仍需要正确加载 Agent / 扩展,并匹配实际使用的类和方法。
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdAdvice.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/SetLogIdAdvice.java · 第 1 行起

3. 怎么知道应该写到哪个 Span?——Context 与当前 Span

一次请求运行时,OpenTelemetry 会维护当前执行上下文。此处帮助类通过 Java8BytecodeBridge.currentSpan() 取得当前 Span,再调用 setAttribute。桥接也帮助应用类加载器中的扩展代码访问 Agent 维护的当前上下文。

Span span = Java8BytecodeBridge.currentSpan();
span.setAttribute("logId", logId);

这两行展示核心动作;实际代码还会判断上下文、空值并截断过长值。可以理解为:当前执行到订单 A 的某次调用时,读到的业务 logId 被挂到这次调用的 Span 上。Alibaba Dubbo 扩展使用 context.makeCurrent(),让真实调用在这个上下文范围内执行。

线程池、异步任务还需要正确传播上下文。ThreadLocal 本身不会把上下文自动传到另一个线程,也不能仅凭 logId 相同就自动重建父子 Span 关系。
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/SpanTagger.java · 第 145 行起
展开实际源码片段
 145    private static Span currentSpan() {
 146      Span span = Java8BytecodeBridge.currentSpan();
 147      if (span == null || !span.getSpanContext().isValid()) {
 148        return null;
 149      }
 150      return span;
 151    }
 152  
 153    private static String truncate(String v) {
 154      return v.length() > MAX_VALUE_LENGTH ? v.substring(0, MAX_VALUE_LENGTH) : v;
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/TracingFilter.java · 第 1 行起

4. 怎么从不同业务对象取商户与订单?——反射与缓存

Dubbo 参数可能是 PayRequest,里面有 memberId、memberTransId。扩展不直接依赖这个业务类,而是在运行时读取对象的 Class,查找字段并读取值。

Dubbo 调用参数对象
    ↓ 取得 Class / 字段信息
匹配 memberId、memberTransId 等字段名
    ↓ 反射读取字段值
写入当前 Span 的附加属性
技术为什么使用
反射在不引入每一种业务 DTO 依赖的情况下读取字段。
ConcurrentHashMap缓存类的字段信息,避免每次调用重新扫描,同时支持并发访问。
递归扫描普通对象在嵌套对象中寻找业务字段。
基于对象身份的集合识别已经访问过的对象,避免相互引用造成无限递归。
最大递归深度控制扫描范围与运行开销。

这不是任意结构通用解析器:嵌套 Map、List、数组等没有完整统一的递归展开逻辑,新增业务字段时应结合真实参数结构验证。

源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/biz/BizFieldExtractor.java · 第 1 行起

5. MDC 起什么作用?——日志与 Span 之间的桥

MDC 可以理解为日志上下文字段容器,常见实现与当前线程关联。例如 MDC.put("logId", "LOG-888"),日志框架便可以把这个字段带入日志事件。这里有两个数据方向:

方向一:
业务 getLogId / setLogId
    → Advice 捕获值
    → 同时补充 Span 与 MDC

方向二:
业务直接调用 MDC.put(...)
    → MdcPutAdvice 拦截
    → 将字段补充到当前 Span

LogIdPuller 还会在日志输出前主动读取已识别的静态 Holder getter,减少采集时机不合适造成的缺字段。MdcBridge 使用类加载器和反射访问应用侧 SLF4J,避免把帮助类直接绑死在某个日志实现上。

源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcPutAdvice.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcBridge.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdPuller.java · 第 1 行起

6. 同一个 Span 出现多个 logId 呢?——ThreadLocal + LinkedHashSet

ThreadLocal 保存当前线程的累积状态,状态内记录 spanId;LinkedHashSet 用于去重并保留出现顺序。比如同一线程、同一 Span 依次看到 A → B → B → C:

logId  = C       // 最近一次的值
logIds = A,B,C   // 累计的不重复值

默认最多累计 8 个不同值,值长度最多 256 字符。当前实现从第二个不同值起才写 logIds 与 logId2 等属性,不生成 logId1。累积状态按线程和 spanId 工作,不是整个分布式 Trace 共享的全局集合;详细边界见“logId 怎样与链路关联”章节。

7. 技术与实际用途对照

技术在这份二开里的用途
Java Agent / Byte Buddy Advice运行时增强 logId 方法、MDC 操作和日志输出方法。
SPI / AutoService注册扩展入口,让 Agent 发现并加载模块。
Dubbo Filter SPI在 RPC 调用前后创建或补充 Span、提取业务字段。
Dubbo attachment + OTel 传播器在 RPC 消息中注入、提取链路上下文,实现跨服务传播。
Context / Scope / Java8BytecodeBridge维护、获取当前 Span,使附加字段落到正确调用上。
反射 / ClassLoader读取业务对象字段、访问应用侧日志框架与已识别的 Holder。
ThreadLocal / LinkedHashSet / ConcurrentHashMap分别用于线程状态、ID 有序去重、并发元数据缓存。
Apollo HTTP 轮询读取 Collector 策略或 Agent 应急采样比例。
SHA-256Collector 比较策略文本是否变化,跳过内容相同的更新。
Go goroutine / channel后台拉取策略,并将更新交给处理循环。
volatile保证 Agent 采样执行线程能看到已替换的 Sampler 引用。
OCB / Go module replace组装精简 Collector,并将组件依赖指向本地二开源码。

8. 面试时可以这样介绍

我们通过 Java Agent 和 Byte Buddy 对日志 ID 的读写方法、MDC 操作进行增强,同时通过 Dubbo Filter 提取业务字段。利用反射读取商户和订单信息,再通过 OpenTelemetry 当前上下文把这些字段写到对应 Span 上,从而建立业务日志、订单与分布式链路之间的关联。Collector 侧扩展了 Apollo 策略轮询,通过内部更新通道实现尾部采样策略的动态替换。

04 · 目录、版本与改动范围

位置职责接手时怎么读
baofoo-otel/agent-extension业务字段、Dubbo 兼容、MonitorLog、应急采样;Gradle 工程。先读模块入口,再读 Filter / Advice / 提取器。
baofoo-otel/logid-ext推荐主线的 OTel logId 扩展;独立外挂 JAR。从 LogIdInstrumentationModule → Advice → SpanTagger / MdcBridge 阅读。
baofoo-otel/logid-agent另一个独立 Byte Buddy premain Agent,Maven 工程。属于另一套实现形态;README 主线构建指向 logid-ext,不能默认两者都应加载。
baofoo-otel/collector-patch尾部采样补丁与 Apollo Provider 的可搬迁副本。修改工作副本后,核对这里是否同步。
baofoo-otel/collector-buildOCB 清单、Windows / Linux 构建、配置重打包、容器入口。尤其先检查绝对路径、目标操作系统和包内文件。
baofoo-otel/config、apollo-policies、deploy应用/Collector 配置、采样模板、启动与分发样例。样例、模板、实际启用配置要分别核对。
upstream/opentelemetry-java-instrumentation上游 Agent 参考源码;本地 Git describe 为 v2.30.0。当前定制通过扩展实现,不要求重新编译整份上游 Agent。
upstream/opentelemetry-collector-contribCollector 真正的本地替换源码。构建清单锁定 v0.157.0;git describe 显示同版本组件标签 receiver/k8sclusterreceiver/v0.157.0。

Collector 的 git status 确认:tailsamplingprocessor/config.go、processor.go、go.mod 已修改;policy_watcher.go 与 confmap/provider/apolloprovider/ 为新增。Java 扩展依赖 Agent 2.30.0、扩展 API 2.30.0-alpha、SDK 1.64.0,目标字节码为 Java 8。

源码:baofoo-otel/agent-extension/build.gradle.kts · 第 1 行起
源码:baofoo-otel/logid-agent/src/main/java/com/baofoo/logid/LogIdAgent.java · 第 30 行起
展开实际源码片段
  30      public static void premain(String agentArgs, Instrumentation inst) {
  31          if (LOG_ID_ENABLED) {
  32              installLogIdHolder(inst);
  33          }
  34          if (MDC_ENABLED) {
  35              installMdcAdapter(inst);
  36          }
  37      }
  38  
  39      private static void installLogIdHolder(Instrumentation inst) {
  40          final String[] tokens = CLASS_TOKENS.split(",");

05 · Java 七个埋点模块

README 提到的“7 个模块”对应 3 个业务扩展与 4 个日志关联扩展。除此之外,agent-extension 中还有一套采样自定义器,它不是这七个 InstrumentationModule 之一。

模块拦截 / 注入位置输出或行为
AlibabaDubboInstrumentationModulecom.alibaba.dubbo.common.extension.ExtensionLoader;注入 Dubbo Filter 资源与帮助类。创建客户端 / 服务端 Span,传播上下文,提取业务字段。
ApacheDubboBizFieldModuleorg.apache.dubbo 的 Filter SPI。在当前已有 Span 上打商户、订单、返回码属性。
MonitorLogInstrumentationModulecom.baofu.log.util.MonitorLogUtil.info 的单参数方法。读取业务监控对象,补充响应码、交易类型等属性。
LogIdInstrumentationModule类名包含默认 HeaderEntity / ThreadContext 的 getLogId、setLogId。业务 logId → 当前 Span + MDC;识别可主动拉取的 getter。
CallAppendersInstrumentationModuleLogback Logger.callAppenders;模块 order=-100。在日志采集前主动拉取当前线程的业务 logId。
MdcInstrumentationModuleorg.slf4j.spi.MDCAdapter 的 put(String,String)。MDC 字段 → 当前 Span;指定 ID 键进入 logId 累积逻辑。
MarkerInstrumentationModuleLogback Logger.buildLoggingEventAndAppend,创建日志事件之前。复杂 Logstash Marker → 临时 MDC;方法退出时清理这些键。

Agent 扩展使用 AutoService 注册入口,Byte Buddy Advice 在方法入口或出口插入逻辑。Dubbo 模块主要通过帮助类和 SPI 资源注入接入 Filter,并非每个模块都直接修改业务方法体。大量采集异常采用吞异常 / suppress Throwable 的方式,优先避免遥测采集打断业务,也意味着失败时可能静默缺字段。

源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/AlibabaDubboInstrumentationModule.java · 第 1 行起
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubboapache/ApacheDubboBizFieldModule.java · 第 1 行起
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/monitorlog/MonitorLogInstrumentationModule.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdInstrumentationModule.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/CallAppendersInstrumentationModule.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcInstrumentationModule.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerInstrumentationModule.java · 第 1 行起

06 · Dubbo 与业务字段提取

Alibaba Dubbo:负责“建 Span + 补字段”

TracingFilter 先判断调用类型与协议,跳过不适用调用和 injvm;随后使用 Instrumenter 创建 Span。在 context.makeCurrent() 的作用域内执行真实 invoker.invoke,结束时记录异常并结束 Span。传播器通过 Dubbo attachment 的 Getter / Setter 读写上下文。

展开实际源码片段
  25    public Result invoke(Invoker<?> invoker, Invocation invocation) {
  26      if (!(invocation instanceof RpcInvocation)) {
  27        return invoker.invoke(invocation);
  28      }
  29  
  30      RpcContext rpcContext = RpcContext.getContext();
  31      if (rpcContext.getUrl() == null || "injvm".equals(rpcContext.getUrl().getProtocol())) {
  32        return invoker.invoke(invocation);
  33      }
  34  
  35      Context parentContext = Context.current();
  36      DubboRequest request = DubboRequest.create((RpcInvocation) invocation, rpcContext);
  37  
  38      if (!instrumenter.shouldStart(parentContext, request)) {
  39        return invoker.invoke(invocation);
  40      }
  41  
  42      Context context = instrumenter.start(parentContext, request);
  43      Span span = Span.fromContext(context);
  44  
  45      try {
  46        Map<String, String> biz = BizFieldExtractor.extractBizFields(invocation.getArguments());
  47        String memberId = biz.get(BizFieldExtractor.MEMBER_ID);
  48        if (memberId != null) {
  49          span.setAttribute(BizFieldExtractor.MEMBER_ID, memberId);
  50        }
  51        String memberTransId = biz.get(BizFieldExtractor.MEMBER_TRANS_ID);
  52        if (memberTransId != null) {
  53          span.setAttribute(BizFieldExtractor.MEMBER_TRANS_ID, memberTransId);
  54        }
  55      } catch (Throwable ignored) {
  56      }
  57  
  58      Result result;
  59      try (Scope ignored = context.makeCurrent()) {
  60        result = invoker.invoke(invocation);
  61      } catch (Throwable e) {
  62        instrumenter.end(context, request, null, e);
  63        throw e;
  64      }
  65  
  66      try {
  67        if (result != null && result.getValue() != null) {
  68          String errorCode = BizFieldExtractor.extractErrorCode(result.getValue());
  69          if (errorCode != null) {
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/DubboSingletons.java · 第 1 行起

Apache Dubbo:负责“给已有 Span 补字段”

BizFieldTaggingFilter 取 Span.current(),对入参提取 memberId、memberTransId,对返回对象提取 errorCode。它自己不 start/end Span,所以要确认官方 Dubbo 埋点已启用、Filter 时序下能取得正确 Span。代码变量 recording 实际检查的是 SpanContext.isValid(),不是 Span.isRecording();有效上下文也可能是未记录的 Span。

源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubboapache/BizFieldTaggingFilter.java · 第 15 行起
展开实际源码片段
  15      Span span = Span.current();
  16      boolean recording = span != null && span.getSpanContext().isValid();
  17  
  18      if (recording) {
  19        try {
  20          Map<String, String> biz = BizFieldExtractor.extractBizFields(invocation.getArguments());
  21          String memberId = biz.get(BizFieldExtractor.MEMBER_ID);
  22          if (memberId != null) {
  23            span.setAttribute(BizFieldExtractor.MEMBER_ID, memberId);
  24          }
  25          String memberTransId = biz.get(BizFieldExtractor.MEMBER_TRANS_ID);
  26          if (memberTransId != null) {
  27            span.setAttribute(BizFieldExtractor.MEMBER_TRANS_ID, memberTransId);
  28          }
  29        } catch (Throwable ignored) {
  30        }
  31      }
  32  
  33      Result result = invoker.invoke(invocation);
  34  
  35      if (recording) {
  36        try {
  37          Object value = result == null ? null : result.getValue();
  38          if (value != null) {
  39            String errorCode = BizFieldExtractor.extractErrorCode(value);
  40            if (errorCode != null) {
  41              span.setAttribute("errorCode", errorCode);
  42            }
  43          }
  44        } catch (Throwable ignored) {
  45        }
  46      }

反射提取的规则与边界

字段 / 场景实际处理
memberId / memberTransId字段名忽略大小写;值支持 String、Long、Integer;递归反射普通对象,访问继承字段。
递归保护MAX_DEPTH=5,使用对象身份集合避免循环引用;缓存 Class 对应的字段列表。
Map 与集合Map 检查当前层条目;并不是任意嵌套 Map / List 都会递归展开。java.* 对象会跳过,数组也没有专门的元素遍历分支。
返回码依次查 errorCode / errCode / respCode / resultCode / code / retCode 的对应字段;不是通用 JSON / Map 返回对象解析器。
业务失败与技术异常写 errorCode 属性不等于把 Span 状态设为 ERROR;Dubbo 路径还会写入成功返回码,因此尾部采样需要正确排除成功码。

07 · MonitorLog 补充了什么

MonitorLog 是业务结果的另一条补充入口:方法调用前读取日志对象,向当前 Span 填充字段。它不创建新的 Span,也不替代业务自身的日志输出。

来源字段目标 Span 属性
respcode 或 resultrespCode;不在成功码集合时另外写 errorCode。
txnType、txnSubTypetxnType、txnSubType。
sponsor / memberidmemberId。
transid / membertransidmemberTransId。

默认成功码为 S、SUCCESS、0000、1、0、00、I,可通过 JVM 系统属性 baofoo.monitorlog.successCodes 调整。字段通过反射读取并缓存。该成功码集合必须与 Collector 里的业务错误判定对齐,否则同一返回码在两条采集入口上可能被不同地理解。

源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/monitorlog/MonitorLogTagger.java · 第 1 行起

08 · logId 怎样与链路关联

三条入口,汇入同一个 SpanTagger

  1. getLogId 返回后:记住可用 getter,再将返回值写入当前 Span 和 MDC。setLogId 进入时:将传入值写入当前 Span 和 MDC。
  2. 日志即将输出时:LogIdPuller 利用已经记住的静态 getter 主动读取当前线程 logId,避免只依赖业务偶然调用 getter 的时机。
  3. 业务直接执行 MDC.put 时:MdcPutAdvice 捕获值,SpanTagger.tagMdc 写 Span;指定的业务日志 ID 键额外进入 tagLogId。

为什么使用 Java8BytecodeBridge

帮助类运行在应用类加载器中,需要通过 Agent 提供的桥接取得当前上下文。SpanTagger 使用 Java8BytecodeBridge.currentSpan();MdcBridge 则通过类加载器反射调用应用侧 SLF4J MDC,避免直接绑死某个日志实现。

同一 Span 出现多个 logId 的例子

执行Span 属性的变化
第一次看到 AlogId=A;只有一个值时不写 logIds,也不生成 logId1。
第二次看到 BlogId=B;logId2=B;logIds=A,B。
第三次看到 ClogId=C;保留 logId2=B;增加 logId3=C;logIds=A,B,C。
连续再次看到 C与上一次相同,提前返回,减少重复 setAttribute。
默认累计超过 8 个不同值logId 仍更新为最新值;累计列表不再扩充,设置 logIdsTruncated=true。
展开实际源码片段
  72    public static void tagLogId(String value) {
  73      if (value == null || value.isEmpty()) {
  74        return;
  75      }
  76      Span span = currentSpan();
  77      if (span == null) {
  78        return;
  79      }
  80      String v = truncate(value);
  81      String spanId = span.getSpanContext().getSpanId();
  82      LogIds acc = LOG_IDS.get();
  83      if (acc == null || !acc.spanId.equals(spanId)) {
  84        acc = new LogIds(spanId);
  85        LOG_IDS.set(acc);
  86      }
  87      // logback 渲染每条日志都会调进来,而绝大多数情况下值根本没变。
  88      // 值没变就一次 span 操作都不做——setAttribute 在 SDK 里是要加锁的,
  89      // 这一层短路比改动前每条日志都写一次还省。
  90      if (v.equals(acc.last)) {
  91        return;
  92      }
  93      acc.last = v;
  94      // 保持原有语义:logId 始终是最近一次见到的值,下游已有查询不受影响
  95      span.setAttribute(LOG_ID_KEY, v);
  96  
  97      if (acc.values.contains(v) || acc.truncated) {
  98        return;
  99      }
 100      if (acc.values.size() >= MAX_LOG_IDS) {
 101        acc.truncated = true;
 102        span.setAttribute(LOG_ID_TRUNCATED_KEY, true);
 103        return;
 104      }
 105      acc.values.add(v);
 106      // 只有一个 logId 时不额外写,避免给绝大多数 span 平白多两个属性
 107      if (acc.values.size() == 1) {
 108        return;
 109      }
 110      // 每个 logId 单独一个属性:logId1 / logId2 / ...
 111      // 不用 logId.1 这种写法——OTel 的点号会被展开成嵌套 JSON,
 112      // 和标量的 logId 撞在同一个 key 上,Doris 侧会类型冲突。
 113      span.setAttribute(indexedKey(acc.values.size()), v);
 114      // 再拼一份逗号分隔的,反查「这个 logId 属于哪条链路」时一个条件就够,不用 OR 一串
 115      span.setAttribute(LOG_ID_ALL_KEY, String.join(",", acc.values));
 116    }
 117  
 118    private static AttributeKey<String> indexedKey(int index) {
 119      return attributeKey(LOG_ID + index);
 120    }
 121  
 122    public static void tagMdc(String key, String value) {
 123      if (key == null || value == null || key.isEmpty() || value.isEmpty()) {
代码注释写着 logId1 / logId2 / …,但当前实现第一个值提前返回,所以不会生成 logId1。查询第一个历史值应看 logIds;仅一个值时看 logId。

不是“整个 Trace 全局收集全部 ID”

  • 累积器是 ThreadLocal,并根据 spanId 切换。父 Span → 子 Span → 父 Span 时,父 Span 的累积状态可能重新建立;同一个 Span 跨线程写入也没有共享的完整集合。
  • 值截断为最多 256 字符;默认累计上限 8。不要把这个字段当成完整、无限制的审计记录。
  • 默认业务 ID 键是 logId、TRACE_LOG_ID、traceLogId;traceId / trace_id 并不在默认名单中。等于当前 traceId / spanId 的 MDC 值会被跳过。
  • 主动拉取需要先学习到可用的 static、无参数、返回 String 的 getter。没有匹配 Holder 或尚未学习到 getter 时,不能保证自动补全。
  • MdcBridge 不等同于完整的请求生命周期管理;应用仍应正确清理自身线程上下文。
展开实际源码片段
 122    public static void tagMdc(String key, String value) {
 123      if (key == null || value == null || key.isEmpty() || value.isEmpty()) {
 124        return;
 125      }
 126      Span span = currentSpan();
 127      if (span == null) {
 128        return;
 129      }
 130      String v = truncate(value);
 131      // 值就是这个 span 自己的 trace/span id 时直接跳过:打上去和 trace_id / span_id 两列完全重复,
 132      // 而且 traceId 会命中 isLogIdKey 被当成 logId 记一遍,造出一个和业务无关的假 logId。
 133      // infra-tracing 每次请求都会 MDC.put("traceId", ...),正好走到这里。
 134      SpanContext ctx = span.getSpanContext();
 135      if (v.equals(ctx.getTraceId()) || v.equals(ctx.getSpanId())) {
 136        return;
 137      }
 138      span.setAttribute(attributeKey(key), v);
 139      if (isLogIdKey(key)) {
 140        // 走同一条累积逻辑,否则 MDC 这条路径进来的 logId 又会退化成只留最后一个
 141        tagLogId(v);
 142      }
 143    }
 144  
 145    private static Span currentSpan() {
 146      Span span = Java8BytecodeBridge.currentSpan();
 147      if (span == null || !span.getSpanContext().isValid()) {
 148        return null;

09 · Marker 与 MDC 的处理时序

MarkerCapture 处理 net.logstash.logback.marker.* 的部分结构化 Marker:提取 Map、JSON 字符串或 Bean,摊平为默认 marker. 前缀的键,先写进 MDC。随后 Logback 创建事件,官方采集器就有机会捕获这些字段。退出 Advice 时再清理临时键。

MarkerAdvice 入口
    → MarkerCapture.put(marker)
    → MDC 中暂时出现 marker.xxx
    → Logback 创建 LoggingEvent / 调用 appender
    → 官方日志采集读取 MDC
MarkerAdvice 出口
    → MarkerCapture.clear(keys)

若 MDC 埋点也启用,这些 put 操作还可进入当前 Span 的属性,因此这里影响的不只是日志字段。普通 SingleField / MapEntries 等部分 Marker 交给官方能力处理;自定义 JSON 解析器避免额外引入 Jackson 到帮助类的类加载环境。

源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerAdvice.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerCapture.java · 第 1 行起
源码:baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/JsonFlattener.java · 第 1 行起
  • 敏感字段过滤存在路径差异:Bean 读取路径检查 deny 名单;Map 和 JSON 路径没有统一执行同样的名单过滤,不能视作已完成全量脱敏。
  • 退出清理使用删除键,而不是恢复覆盖前的 MDC 旧值;相同键名碰撞或嵌套日志场景需要注意。
  • MAX_FIELDS、MAX_DEPTH 用于限制展开,但并非所有路径的统一最终总量闸门。需要增加字段时,优先确认体积与字段范围。

10 · Agent 应急采样:README 外的重要功能

EmergencySamplerProvider 通过 AutoConfigurationCustomizerProvider 替换 SDK 的 Sampler。它根据显式 baofoo.sampler.endpoint,或 baofoo.sampler.env / env 的 DEV、FAT、UAT、PRO 映射定位 Apollo;没有可识别配置时不启用这套动态包装。

参数 / 比例实际语义
baofoo.sampler.key默认 otel.agent.sampling.ratio。
轮询与超时默认 60 秒轮询一次、5 秒请求超时;daemon 线程运行;拉取失败保留当前采样器。
ratio ≥ 1恢复原有 defaultSampler;只有原配置为 always_on 时才等于恢复全量。
0 < ratio < 1切换到 parentBased(traceIdRatioBased(ratio))。
ratio ≤ 0切换到 parentBased(alwaysOff());已有 sampled 父上下文仍可能被继承,不能理解为强制丢弃每个 Span。
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/EmergencySamplerProvider.java · 第 1 行起
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/ApolloEndpointResolver.java · 第 1 行起
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/EmergencySampler.java · 第 25 行起
展开实际源码片段
  25    private void applyRatio(double newRatio) {
  26      if (newRatio >= 1.0d) {
  27        this.current = defaultSampler;
  28      } else if (newRatio <= 0.0d) {
  29        this.current = Sampler.parentBased(Sampler.alwaysOff());
  30      } else {
  31        this.current = Sampler.parentBased(Sampler.traceIdRatioBased(newRatio));
  32      }
  33      this.ratio = newRatio;
  34    }
源码:baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/RatioWatcher.java · 第 27 行起
展开实际源码片段
  27      this.key = System.getProperty("baofoo.sampler.key", "otel.agent.sampling.ratio");
  28      this.intervalSeconds = intProp("baofoo.sampler.interval.seconds", 60);
  29      this.timeoutMillis = intProp("baofoo.sampler.timeout.seconds", 5) * 1000;
Agent 头部采样发生在请求结果未知时。若根请求已被它丢弃,Collector 后面即使配置“保留所有错误 / 慢请求”也无法找回这些 Span。应急降采样与错误链路完整保留之间存在取舍。

这组 baofoo.* 参数在代码里通过 System.getProperty 读取,应使用 JVM -D 参数等方式设置系统属性;不能仅因为写进 otel-agent.properties 就假定自定义代码一定读得到。比例解析没有统一的有限数值 / [0,1] 校验,后续维护可补上明确校验与失败日志。

11 · Collector 尾部采样怎样热更新

补丁的四个落点

文件作用
config.go新增 policy_source 配置:endpoint、key、interval、timeout。
policy_watcher.go轮询远端 → 提取策略文本 → 内容摘要比较 → 解析、校验与提交。
processor.goStart 启动 watcher;setSamplingPolicyE 返回错误;使用 newPolicyChan 将策略送入处理循环;Shutdown 先停 watcher。
go.mod显式依赖 YAML 解析库。
源码:upstream/opentelemetry-collector-contrib/processor/tailsamplingprocessor/config.go · 第 371 行起
源码:upstream/opentelemetry-collector-contrib/processor/tailsamplingprocessor/processor.go · 第 267 行起
展开实际源码片段
 267  func (tsp *tailSamplingSpanProcessor) setSamplingPolicyE(cfgs []PolicyCfg) error {
 268  	policies, err := tsp.loadSamplingPolicies(tsp.host, cfgs)
 269  	if err != nil {
 270  		return err
 271  	}
 272  	tsp.newPolicyChan <- newPolicyCmd{policies: policies}
 273  	return nil
 274  }
 275  

一次更新的执行过程

  1. 启动后立即尝试拉取,之后按 interval 定期执行;默认间隔 30 秒,超时 5 秒。
  2. 如果返回 Apollo JSON,读取 configurations[key];也支持直接返回策略文本。
  3. 对提取后的文本计算 SHA-256,与上次成功提交的内容比较;文本不变则跳过。
  4. 接受三种形态:裸 policies 列表、包含 policies 的对象、包含 processors.tail_sampling.policies 的完整配置。
  5. YAML 解析失败、空策略或策略构造失败时保留旧策略;成功后通过 newPolicyChan 提交,并记录摘要。
  6. 处理循环消费命令,将 tsp.policies 替换为新的策略集合;不为这次更新重建整个 Collector。
源码:baofoo-otel/collector-patch/tailsamplingprocessor/policy_watcher.go · 第 98 行起
展开实际源码片段
  98  	policies, err := parsePolicies(body)
  99  	if err != nil {
 100  		w.logger.Error("failed to parse sampling policies, keeping current ones", zap.Error(err))
 101  		return
 102  	}
 103  	if len(policies) == 0 {
 104  		w.logger.Warn("remote sampling policies are empty, keeping current ones")
 105  		return
 106  	}
 107  
 108  	if err := w.apply(policies); err != nil {
 109  		w.logger.Error("remote sampling policies rejected, keeping current ones", zap.Error(err))
 110  		return
 111  	}
 112  	w.lastSum = cur
 113  	w.logger.Info("sampling policies hot-reloaded", zap.Int("count", len(policies)))
 114  }
 115  
展开实际源码片段
 594  		// Drain any pending configuration updates before processing the batch.
 595  		// Go's select is non-deterministic, so when both workChan and a config
 596  		// update channel are ready simultaneously, the workChan case may be
 597  		// selected before the config update is applied. By draining config
 598  		// channels here (non-blocking), we ensure that calls like
 599  		// SetMaximumTraceSizeBytes or SetSamplingPolicy are always reflected
 600  		// before the incoming traces are evaluated against them.
 601  		select {
 602  		case cmd := <-tsp.newPolicyChan:
 603  			tsp.policies = cmd.policies
 604  			tsp.logger.Debug("New policies loaded", zap.Int("policies.len", len(tsp.policies)))
 605  		default:
 606  		}
 607  		select {
 608  		case maxTraceSize := <-tsp.newTraceSizeChan:
 609  			tsp.maxTraceSizeBytes = maxTraceSize
 610  			tsp.logger.Debug("New maximum trace size loaded", zap.Uint64("size", maxTraceSize))
热更新的是 policies 集合,不是整个服务配置。decision_wait、num_traces、接收端口、Doris 地址与 pipeline 等变更不能靠此入口一并生效。已缓存的采样决定也不会因此重新计算;热更新不意味着所有在途或历史 Trace 都按新策略重判。

当前 watcher 源码在 collector-patch 与 upstream 工作副本中一致。日志“hot-reloaded”表示策略构造成功并已提交到更新通道;代码没有额外等待处理循环确认完成的应答机制。整份配置里有多个 tail_sampling/* 时,解析器寻找匹配处理器,不宜依赖它自动选中你想更新的那一个;发布明确的 policies 文档更清楚。

12 · Apollo 的三个入口不要混淆

入口读取内容何时生效不具备的能力
Agent RatioWatcher一个采样比例运行中周期轮询,切换 Sampler。无法根据尚未发生的业务错误决定是否保留。
Collector policy_source尾部采样 policies运行中周期轮询,通过策略通道替换。不会重建整个 pipeline。
Collector apollo: Provider整份 Collector YAML配置系统调用 Retrieve 时加载。WatcherFunc 参数未使用;本实现没有远程全配置持续热更新。
APOLLO_META=http://<Apollo地址>
otelcol-slim --config="apollo:<appId>/<cluster>/<namespace.yml>"

Provider 要求 URI 中 appId、cluster、namespace 三段齐全,从 APOLLO_META 拼接 /configs/... 请求,并从 configurations.content 取得 YAML。请求超时为 10 秒;404、缺少 content 或空配置会返回错误。这与 policy_source 的失败后保留旧策略不是同一个启动阶段。

源码:upstream/opentelemetry-collector-contrib/confmap/provider/apolloprovider/provider.go · 第 64 行起
展开实际源码片段
  64  func (p *provider) Retrieve(ctx context.Context, uri string, _ confmap.WatcherFunc) (*confmap.Retrieved, error) {
  65  	if !strings.HasPrefix(uri, schemeName+":") {
  66  		return nil, fmt.Errorf("%q uri is not supported by %q provider", uri, schemeName)
  67  	}
  68  	path := strings.TrimPrefix(uri, schemeName+":")
  69  
  70  	meta := strings.TrimSpace(os.Getenv(metaEnv))
  71  	if meta == "" {
  72  		return nil, fmt.Errorf("%s must be set to the apollo config service address, e.g. http://a-config.baofoo.cn:18080", metaEnv)
  73  	}
  74  	meta = strings.TrimSuffix(meta, "/")
  75  
  76  	if err := validatePath(path); err != nil {
  77  		return nil, err
  78  	}
  79  
  80  	content, err := p.fetchContent(ctx, meta+"/configs/"+path)
  81  	if err != nil {
  82  		return nil, err
  83  	}
  84  
  85  	// content 是一段 YAML 文本,解析成 map 交给 Collector
  86  	var conf map[string]any
  87  	if err := yaml.Unmarshal([]byte(content), &conf); err != nil {
  88  		return nil, fmt.Errorf("apollo namespace %q does not contain valid yaml: %w", path, err)
  89  	}
  90  	if len(conf) == 0 {
  91  		return nil, fmt.Errorf("apollo namespace %q is empty", path)
  92  	}
  93  	return confmap.NewRetrieved(conf)
  94  }
  95  
  96  // validatePath 要求 <appId>/<cluster>/<namespace> 三段都在,
  97  // 少一段的话 Apollo 会返回 404,报错很含糊,不如提前挡掉。

两份 provider.go 的文件哈希不同,但逐行 diff 显示差异是注释与 package import 注释,当前可见执行逻辑一致;不能只凭哈希不同就断言存在功能漂移。

13 · 配置和采样模板的实际差别

策略文件 / 场景保留条件需要注意
apollo-policies 日常模板ERROR + 业务错误 + ≥1000ms 慢链路 + 普通流量 10%。业务成功码排除集合较完整。
全量模板100% 保留。只能保留到达 Collector 且未被前置过滤移除的数据。
高峰模板ERROR + ≥3000ms + 普通流量 1%。没有日常模板中的 keep-biz-errors 独立策略。
指定商户模板ERROR + ≥1000ms + 指定 memberId + 普通流量 10%。也没有 keep-biz-errors 独立策略;只写业务 errorCode 的失败可能按普通比例保留。
config/apollo-tailsampling.yml 与 prod-bx 本地回退策略ERROR + 业务错误 + ≥500ms + 普通流量 10%。与日常模板的 1000ms 阈值不同;业务成功码只排除 0000、00、S。

对本仓库这些独立正向保留策略,可理解为满足任一保留条件就有机会被采样;并不是所有条件必须同时满足。不要把这一说明推广到任意包含 drop / composite / 特殊决策语义的上游复杂策略。

已发现的对齐点:MonitorLog 与日常模板把 0、1、SUCCESS、I 也视为成功,但 prod-bx 的业务错误策略没有排除这些值。Dubbo 返回码提取又会把成功码写到 errorCode,可能导致部分成功请求被当成业务错误保留。应由业务统一成功码,再统一各处配置。
源码:baofoo-otel/config/apollo-tailsampling.yml · 第 1 行起
展开实际源码片段
  27    tail_sampling:
  28      # 收到某条 trace 的第一个 span 后,等待多久做判定。
  29      # 等待期间该 trace 的所有 span 驻留内存。
  30      # 应大于绝大多数请求的耗时,否则慢请求会在结束前被判定。
  31      decision_wait: 10s
  32      # 内存中同时保留的 trace 数上限,环形缓冲。
  33      # 超出后最早的 trace 会被挤出,表现为链路残缺。
  34      # 内存占用约等于 num_traces × 每条 trace 的平均 span 数。
  35      num_traces: 50000
  36      # 预估的每秒新增 trace 数,用于内部结构预分配。
  37      expected_new_traces_per_sec: 100
  38  
  39      # 从 Apollo 拉取策略并热替换(本地二开功能)。
  40      # 修改策略后无需重启容器,其余配置项的修改仍需重启。
  41      policy_source:

prod-bx 的两组业务流水线

主组 tail_sampling:decision_wait=10s,num_traces=50000,expected_new_traces_per_sec=100;RM 组分别是 10s、10000、30。二者各有 policy_source。按命名空间及部分服务名规则路由到 doris / doris/rm,分别写主库 otel_hina 与 RM 库 otel_rm。

Trace:memory_limiter → 业务分流 filter → filter/traces → tail_sampling → batch → Doris
Log:  memory_limiter → 业务分流 filter → filter/logs → transform → batch/bulk → Doris
Metric:memory_limiter → 业务分流 filter → batch/bulk → Doris

filter/traces 会移除 Redis 心跳、部分消息中间件操作及 Nacos/Apollo/Eureka 等噪音请求;filter/logs 丢低于 INFO 的日志,并存在针对指定 Nacos ERROR logger 的临时过滤规则。因为过滤在采样之前,“保留 ERROR”不代表被前置过滤掉的错误数据还能恢复。

14 · 构建与交付:接手时怎么操作

Agent:只构建扩展与组装包

cd D:/baofu/otel-src-all/baofoo-otel/agent-extension
./gradlew.bat assemble

cd D:/baofu/otel-src-all/baofoo-otel/logid-ext
./gradlew.bat assemble
产物角色接入方式
baofoo-otel-ext.jar业务与应急采样扩展。extendedAgent 任务把它放进官方 Agent 的 extensions/。
opentelemetry-javaagent-baofoo.jar已组装业务扩展的完整 Agent。作为 -javaagent 指定。
baofoo-logid-ext.jar日志 ID 外挂扩展。通过 -Dotel.javaagent.extensions 指定路径。
java -javaagent:/opt/otel/opentelemetry-javaagent-baofoo.jar \
  -Dotel.javaagent.extensions=/opt/otel/baofoo-logid-ext.jar \
  -Dotel.javaagent.configuration-file=/opt/otel/otel-agent.properties \
  -jar app.jar

以上是接入形态示例,路径需按部署目录配置。Gradle 运行 JDK 按 README 要求 17+,生成代码字节码目标为 8;gradle.properties 固定了 127.0.0.1:7890 代理。Apache Dubbo 的 checkDubboVersions 是编译兼容检查,不等于对应版本的运行验证。

源码:baofoo-otel/logid-ext/build.gradle.kts · 第 1 行起
源码:baofoo-otel/agent-extension/gradle.properties · 第 1 行起

Collector:先修正工作目录,再生成和编译

slim-builder.yaml 的 output_path 和两条 replaces 目前仍为 D:/otel-src/...。这份目录实际位于 D:/baofu/otel-src-all/...,构建前应明确改为当前目录。本文只记录问题,没有替你修改构建配置。

output_path: D:/baofu/otel-src-all/baofoo-otel/collector-build/slim-build

replaces:
  - github.com/open-telemetry/opentelemetry-collector-contrib/processor/tailsamplingprocessor => D:/baofu/otel-src-all/upstream/opentelemetry-collector-contrib/processor/tailsamplingprocessor
  - github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/apolloprovider => D:/baofu/otel-src-all/upstream/opentelemetry-collector-contrib/confmap/provider/apolloprovider
  1. 准备 Go 与 OCB builder,按模块中的 go.mod 要求选择工具链;本地 Apollo Provider 声明 Go 1.25.0。
  2. build-slim.sh 先生成 main.go / go.mod,再插入 _ "time/tzdata",最后 CGO_ENABLED=0 编译。要核实生成文件确实包含该导入。
  3. 当前 build-slim.sh 写出的文件名是 otelcol-slim.exe;未强制 GOOS,实际目标仍取决于构建环境。README 中不带 .exe 的名称不能直接套用。
  4. Linux 版本通过 build-linux.bat 设置 GOOS=linux、GOARCH=amd64,并依赖已生成的 main.go。
  5. pack-prod-bx.sh 需要已有基准包;默认替换配置与 entrypoint.sh,转换为 LF、写 version.txt、备份旧包;-s 才会替换二进制。
源码:baofoo-otel/collector-build/slim-builder.yaml · 第 1 行起
源码:baofoo-otel/collector-build/build-slim.sh · 第 1 行起
源码:baofoo-otel/collector-build/build-linux.bat · 第 1 行起
源码:baofoo-otel/collector-build/pack-prod-bx.sh · 第 1 行起

time/tzdata 将时区数据嵌入 Go 程序,减少精简容器缺少系统时区数据导致 Doris Asia/Shanghai 配置失败的问题。Windows 本地运行正常不能证明精简 Linux 镜像也正常。当前目录没有生成好的 slim-build / dist,可复现构建和包完整性仍待实际验证。

15 · 文档与源码核对结果

发现依据与影响建议
构建绝对路径仍是旧目录slim-builder.yaml 中 output_path / replaces 指向 D:/otel-src。构建前改成当前根目录,避免编到另一份源码。
组件数量描述不一致README / 清单注释称 12;实际为 1 receiver + 5 processors + 5 exporters + 3 extensions = 14,另有 4 providers。按分类数说明,别把 Provider 与流水线组件混成一个含糊数字。
Agent 应急采样没有在 README 主线单列sampler 包具备独立轮询、系统属性和采样切换。增加使用前提、比例含义与头尾采样关系。
脚本产物名与 README 不完全一致build-slim.sh 输出 .exe;README 示例不带扩展名。明确 Windows 本地包和 Linux 交付包。
启动脚本未完成统一接入start-app-otel.sh 的 jar 分支仍是 SkyWalking;tomcat 分支挂 OTel;jetty 分支未显式增加 OTel 参数。逐分支确认最终 Java 启动命令;脚本设置 OTEL_* 不代表 Agent 已加载。
日志扩展接入需单独确认tomcat 分支只指定 Agent 文件;该行未指定 logid-ext 外挂路径。检查分发包及其他 JVM 配置是否补齐扩展,不能仅凭这行断言 logId 功能已启用。
采样模板和生产回退配置不同慢请求 1000ms / 500ms;成功码集合不同;高峰/商户模板缺业务错误策略。统一业务语义,并记录环境差异。
多 ID 的注释比实现承诺更宽无 logId1;ThreadLocal + spanId 的累积不是全 Trace 集合。按实际字段设计查询与示例。
Marker 的敏感字段过滤不统一Bean 过滤名单不覆盖全部 Map / JSON 写入路径。若需要统一过滤,在公共写入位置集中处理。
Provider 两份文件哈希不同但逻辑一致逐行 diff 为注释 / import 注释差异;watcher 两份一致。后续同步补丁时检查语义差异,不能只凭哈希判断。
源码:baofoo-otel/deploy/start-app-otel.sh · 第 81 行起
展开实际源码片段
  81        if [ $start_type = "jar" ];then
  82           echo "is is jar"
  83           java \
  84               -XX:+UseContainerSupport -XX:InitialRAMPercentage=30.0 -XX:MaxRAMPercentage=70.0 -Xss256k -XX:MetaspaceSize=128m \
  85               -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
  86               -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/tmp/jvm.log -XX:+HeapDumpOnOutOfMemoryError \
  87               -Djute.maxbuffer=666666666 \
  88               -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=4445 \
  89               -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false \
  90               -javaagent:/pinpoint-agent/skywalking-agent/skywalking-agent.jar \
  91               -Dskywalking.agent.service_name=${PROJECT_NAME} \
  92               -Dskywalking.plugin.kafka.bootstrap_servers=10.254.166.138:9092 \
  93               -XX:HeapDumpPath=/tmp/heapdump.hprof \
  94               -jar /data/appdeploy/${PROJECT_JAR} > $JETTY_LOGS/$PROJECT_NAME.log
  95        fi
以上是源码与配置核对结果。没有把目录里的历史 ZIP、README 的体积/耗时记录或注释描述当成经过本次运行验证的事实;也没有在总结中复制部署配置的密码。

16 · 后续二开从哪里下手

你要改什么优先修改位置需要一起考虑
增加商户或订单字段别名BizFieldExtractor / MonitorLogTagger。字段类型、嵌套对象、Map/List 结构;下游查询字段名保持一致。
支持新的 logId HolderLogIdInstrumentationModule 的匹配与 Advice;LogIdPuller。getter 是否 static、类加载器、是否真的执行过识别方法、日志前后时序。
修改日志 ID 累积方式SpanTagger。容量、截断、嵌套 Span / 异步线程、旧查询兼容。
增加复杂日志字段MarkerCapture / JsonFlattener。统一敏感字段控制、总字段数、MDC 原值恢复。
调整错误/慢请求保留Apollo policies 或对应环境本地回退配置。成功码统一;是否在前置 filter 已丢弃;Agent 是否提前降采样。
增加 Collector 组件slim-builder.yaml 并重新生成编译。仅修改 YAML pipeline 无法凭空加载未编译进二进制的组件。
升级上游版本两个 Gradle 依赖版本、Collector 清单与 replace 模块。Advice 匹配、Dubbo SPI、日志调用签名、Collector 补丁接口都需要复核。

适合这套代码的验证样例

  • 用 Alibaba 2.6 与 Apache Dubbo 分别发起一次跨服务请求,确认父子关系、传播以及三个业务字段。
  • 同线程先设置 logId=A,再切换 B,查看 Span 的 logId/logIds 与两条日志;再测试父子 Span 和线程池路径。
  • 分别以 Bean、Map、JSON Marker 记录同样字段,检查字段数量、清理、敏感字段规则是否一致。
  • Apollo 发布有效策略、无效 YAML、空策略,再模拟短暂不可用,观察是否保持旧策略及恢复更新。
  • 在默认采样器与不同上游 sampled 标志下测试 ratio=1、0.1、0,确认 parentBased 的实际效果。
  • 检查 Linux 包包含二进制、otel-config.yaml、entrypoint.sh、version.txt,并验证最小镜像中的时区和最终启动参数。

这些是根据实现推导的后续验证建议,本次没有执行。两个自定义 Gradle 扩展目录未发现 src/test;上游自带测试不能自动证明所有本地改动已覆盖。

17 · 源码索引与阅读顺序

建议顺序:README → agent-extension/build.gradle.kts → Dubbo / MonitorLog → LogIdAdvice 与 SpanTagger → policy_watcher → processor 更新通道 → slim-builder 与 prod-bx 配置。上文可折叠片段直接摘自本地源码并显示行号。

下列链接是相对本 HTML 的本地源码路径,移动整个目录后仍可使用。浏览器可能将源码作为文本打开或下载;详细阅读建议用编辑器定位标注行号。