01 · 这份二开解决了什么
这套方案围绕两个实际问题展开:让技术链路与业务日志、商户、订单关联起来;让采样策略能够随排障和流量压力调整。 Java 侧通过 Agent 扩展在运行时补充字段,Collector 侧通过本地补丁接入 Apollo,最后将遥测数据写入 Doris。
| 关注点 | 当前代码的做法 | 实际价值 |
|---|---|---|
| 业务关联 | 给当前 Span 补充 logId、memberId、memberTransId、errorCode;将部分字段桥接到 MDC。 | 可以从日志 ID 找到相关 Span,再利用 traceId 查询链路;业务字段支持筛选与采样。 |
| 框架兼容 | Alibaba Dubbo 2.6 补建链路;Apache Dubbo 在已有 Span 上补业务字段。 | 兼容历史框架,减少应用逐个改代码的工作。 |
| 采样控制 | Collector 尾部采样热更新 + Agent 应急头部采样。 | 正常情况下按错误、慢请求、比例保留;应急时更早控制产生的数据量。 |
| 交付 | 官方 Java Agent + 自定义扩展;OCB 组装精简 Collector。 | Agent 与 Collector 的二开边界分开,便于升级与追踪改动。 |
02 · 架构与一条请求的经过
Dubbo / MonitorLog 扩展
logId / MDC / Marker 扩展
Trace:尾部采样 → 批处理
链路、日志、指标分别写表
- 请求进入应用后,官方埋点或 Alibaba Dubbo 扩展建立 Span,并维持当前 Context。
- 调用 Dubbo 时,扩展从参数对象中找 memberId、memberTransId;调用结束后从返回对象提取结果码。
- 应用读取或设置业务 logId 时,Advice 将值写入当前 Span,并尝试放入 MDC;日志输出前还会主动拉取已识别的 Holder。
- Logback 官方采集读取 MDC / Marker 等内容生成日志记录。Trace 与 Log 独立上报,是否保留 Trace 不自动决定是否保留 Log。
- Collector 先按命名空间等条件分流、过滤噪音,再对 Trace 等待并做尾部采样,最后批量写入对应 Doris 库。
关联的边界:代码提供“将业务 ID 写入遥测数据”的能力,完整检索还需要存储字段、查询语句和界面配合。本目录不能单独证明“任意日志 ID 都一定能查到完整链路”;采样、上下文丢失、Holder 未识别都会影响结果。
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-9992. 不修改业务源码,如何取到 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 / 扩展,并匹配实际使用的类和方法。
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(),让真实调用在这个上下文范围内执行。
展开实际源码片段
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;4. 怎么从不同业务对象取商户与订单?——反射与缓存
Dubbo 参数可能是 PayRequest,里面有 memberId、memberTransId。扩展不直接依赖这个业务类,而是在运行时读取对象的 Class,查找字段并读取值。
Dubbo 调用参数对象
↓ 取得 Class / 字段信息
匹配 memberId、memberTransId 等字段名
↓ 反射读取字段值
写入当前 Span 的附加属性| 技术 | 为什么使用 |
|---|---|
| 反射 | 在不引入每一种业务 DTO 依赖的情况下读取字段。 |
| ConcurrentHashMap | 缓存类的字段信息,避免每次调用重新扫描,同时支持并发访问。 |
| 递归扫描普通对象 | 在嵌套对象中寻找业务字段。 |
| 基于对象身份的集合 | 识别已经访问过的对象,避免相互引用造成无限递归。 |
| 最大递归深度 | 控制扫描范围与运行开销。 |
这不是任意结构通用解析器:嵌套 Map、List、数组等没有完整统一的递归展开逻辑,新增业务字段时应结合真实参数结构验证。
5. MDC 起什么作用?——日志与 Span 之间的桥
MDC 可以理解为日志上下文字段容器,常见实现与当前线程关联。例如 MDC.put("logId", "LOG-888"),日志框架便可以把这个字段带入日志事件。这里有两个数据方向:
方向一:
业务 getLogId / setLogId
→ Advice 捕获值
→ 同时补充 Span 与 MDC
方向二:
业务直接调用 MDC.put(...)
→ MdcPutAdvice 拦截
→ 将字段补充到当前 SpanLogIdPuller 还会在日志输出前主动读取已识别的静态 Holder getter,减少采集时机不合适造成的缺字段。MdcBridge 使用类加载器和反射访问应用侧 SLF4J,避免把帮助类直接绑死在某个日志实现上。
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-256 | Collector 比较策略文本是否变化,跳过内容相同的更新。 |
| Go goroutine / channel | 后台拉取策略,并将更新交给处理循环。 |
| volatile | 保证 Agent 采样执行线程能看到已替换的 Sampler 引用。 |
| OCB / Go module replace | 组装精简 Collector,并将组件依赖指向本地二开源码。 |
8. 面试时可以这样介绍
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-build | OCB 清单、Windows / Linux 构建、配置重打包、容器入口。 | 尤其先检查绝对路径、目标操作系统和包内文件。 |
| baofoo-otel/config、apollo-policies、deploy | 应用/Collector 配置、采样模板、启动与分发样例。 | 样例、模板、实际启用配置要分别核对。 |
| upstream/opentelemetry-java-instrumentation | 上游 Agent 参考源码;本地 Git describe 为 v2.30.0。 | 当前定制通过扩展实现,不要求重新编译整份上游 Agent。 |
| upstream/opentelemetry-collector-contrib | Collector 真正的本地替换源码。 | 构建清单锁定 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。
展开实际源码片段
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 之一。
| 模块 | 拦截 / 注入位置 | 输出或行为 |
|---|---|---|
| AlibabaDubboInstrumentationModule | com.alibaba.dubbo.common.extension.ExtensionLoader;注入 Dubbo Filter 资源与帮助类。 | 创建客户端 / 服务端 Span,传播上下文,提取业务字段。 |
| ApacheDubboBizFieldModule | org.apache.dubbo 的 Filter SPI。 | 在当前已有 Span 上打商户、订单、返回码属性。 |
| MonitorLogInstrumentationModule | com.baofu.log.util.MonitorLogUtil.info 的单参数方法。 | 读取业务监控对象,补充响应码、交易类型等属性。 |
| LogIdInstrumentationModule | 类名包含默认 HeaderEntity / ThreadContext 的 getLogId、setLogId。 | 业务 logId → 当前 Span + MDC;识别可主动拉取的 getter。 |
| CallAppendersInstrumentationModule | Logback Logger.callAppenders;模块 order=-100。 | 在日志采集前主动拉取当前线程的业务 logId。 |
| MdcInstrumentationModule | org.slf4j.spi.MDCAdapter 的 put(String,String)。 | MDC 字段 → 当前 Span;指定 ID 键进入 logId 累积逻辑。 |
| MarkerInstrumentationModule | Logback Logger.buildLoggingEventAndAppend,创建日志事件之前。 | 复杂 Logstash Marker → 临时 MDC;方法退出时清理这些键。 |
Agent 扩展使用 AutoService 注册入口,Byte Buddy Advice 在方法入口或出口插入逻辑。Dubbo 模块主要通过帮助类和 SPI 资源注入接入 Filter,并非每个模块都直接修改业务方法体。大量采集异常采用吞异常 / suppress Throwable 的方式,优先避免遥测采集打断业务,也意味着失败时可能静默缺字段。
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) {Apache Dubbo:负责“给已有 Span 补字段”
BizFieldTaggingFilter 取 Span.current(),对入参提取 memberId、memberTransId,对返回对象提取 errorCode。它自己不 start/end Span,所以要确认官方 Dubbo 埋点已启用、Filter 时序下能取得正确 Span。代码变量 recording 实际检查的是 SpanContext.isValid(),不是 Span.isRecording();有效上下文也可能是未记录的 Span。
展开实际源码片段
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 或 result | respCode;不在成功码集合时另外写 errorCode。 |
| txnType、txnSubType | txnType、txnSubType。 |
| sponsor / memberid | memberId。 |
| transid / membertransid | memberTransId。 |
默认成功码为 S、SUCCESS、0000、1、0、00、I,可通过 JVM 系统属性 baofoo.monitorlog.successCodes 调整。字段通过反射读取并缓存。该成功码集合必须与 Collector 里的业务错误判定对齐,否则同一返回码在两条采集入口上可能被不同地理解。
08 · logId 怎样与链路关联
三条入口,汇入同一个 SpanTagger
- getLogId 返回后:记住可用 getter,再将返回值写入当前 Span 和 MDC。setLogId 进入时:将传入值写入当前 Span 和 MDC。
- 日志即将输出时:LogIdPuller 利用已经记住的静态 getter 主动读取当前线程 logId,避免只依赖业务偶然调用 getter 的时机。
- 业务直接执行 MDC.put 时:MdcPutAdvice 捕获值,SpanTagger.tagMdc 写 Span;指定的业务日志 ID 键额外进入 tagLogId。
为什么使用 Java8BytecodeBridge
帮助类运行在应用类加载器中,需要通过 Agent 提供的桥接取得当前上下文。SpanTagger 使用 Java8BytecodeBridge.currentSpan();MdcBridge 则通过类加载器反射调用应用侧 SLF4J MDC,避免直接绑死某个日志实现。
同一 Span 出现多个 logId 的例子
| 执行 | Span 属性的变化 |
|---|---|
| 第一次看到 A | logId=A;只有一个值时不写 logIds,也不生成 logId1。 |
| 第二次看到 B | logId=B;logId2=B;logIds=A,B。 |
| 第三次看到 C | logId=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()) {不是“整个 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 到帮助类的类加载环境。
- 敏感字段过滤存在路径差异: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。 |
展开实际源码片段
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 }展开实际源码片段
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;这组 baofoo.* 参数在代码里通过 System.getProperty 读取,应使用 JVM -D 参数等方式设置系统属性;不能仅因为写进 otel-agent.properties 就假定自定义代码一定读得到。比例解析没有统一的有限数值 / [0,1] 校验,后续维护可补上明确校验与失败日志。
11 · Collector 尾部采样怎样热更新
补丁的四个落点
| 文件 | 作用 |
|---|---|
| config.go | 新增 policy_source 配置:endpoint、key、interval、timeout。 |
| policy_watcher.go | 轮询远端 → 提取策略文本 → 内容摘要比较 → 解析、校验与提交。 |
| processor.go | Start 启动 watcher;setSamplingPolicyE 返回错误;使用 newPolicyChan 将策略送入处理循环;Shutdown 先停 watcher。 |
| go.mod | 显式依赖 YAML 解析库。 |
展开实际源码片段
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 一次更新的执行过程
- 启动后立即尝试拉取,之后按 interval 定期执行;默认间隔 30 秒,超时 5 秒。
- 如果返回 Apollo JSON,读取 configurations[key];也支持直接返回策略文本。
- 对提取后的文本计算 SHA-256,与上次成功提交的内容比较;文本不变则跳过。
- 接受三种形态:裸 policies 列表、包含 policies 的对象、包含 processors.tail_sampling.policies 的完整配置。
- YAML 解析失败、空策略或策略构造失败时保留旧策略;成功后通过 newPolicyChan 提交,并记录摘要。
- 处理循环消费命令,将 tsp.policies 替换为新的策略集合;不为这次更新重建整个 Collector。
展开实际源码片段
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))当前 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 的失败后保留旧策略不是同一个启动阶段。
展开实际源码片段
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 / 特殊决策语义的上游复杂策略。
展开实际源码片段
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 是编译兼容检查,不等于对应版本的运行验证。
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
- 准备 Go 与 OCB builder,按模块中的 go.mod 要求选择工具链;本地 Apollo Provider 声明 Go 1.25.0。
- build-slim.sh 先生成 main.go / go.mod,再插入 _ "time/tzdata",最后 CGO_ENABLED=0 编译。要核实生成文件确实包含该导入。
- 当前 build-slim.sh 写出的文件名是 otelcol-slim.exe;未强制 GOOS,实际目标仍取决于构建环境。README 中不带 .exe 的名称不能直接套用。
- Linux 版本通过 build-linux.bat 设置 GOOS=linux、GOARCH=amd64,并依赖已生成的 main.go。
- pack-prod-bx.sh 需要已有基准包;默认替换配置与 entrypoint.sh,转换为 LF、写 version.txt、备份旧包;-s 才会替换二进制。
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 两份一致。 | 后续同步补丁时检查语义差异,不能只凭哈希判断。 |
展开实际源码片段
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 fi16 · 后续二开从哪里下手
| 你要改什么 | 优先修改位置 | 需要一起考虑 |
|---|---|---|
| 增加商户或订单字段别名 | BizFieldExtractor / MonitorLogTagger。 | 字段类型、嵌套对象、Map/List 结构;下游查询字段名保持一致。 |
| 支持新的 logId Holder | LogIdInstrumentationModule 的匹配与 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 的本地源码路径,移动整个目录后仍可使用。浏览器可能将源码作为文本打开或下载;详细阅读建议用编辑器定位标注行号。
- README.md
- baofoo-otel/config/otel-collector-prod-bx.yaml
- baofoo-otel/agent-extension/build.gradle.kts
- baofoo-otel/logid-agent/src/main/java/com/baofoo/logid/LogIdAgent.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/AlibabaDubboInstrumentationModule.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubboapache/ApacheDubboBizFieldModule.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/monitorlog/MonitorLogInstrumentationModule.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdInstrumentationModule.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/CallAppendersInstrumentationModule.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcInstrumentationModule.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerInstrumentationModule.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/TracingFilter.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubbo26/DubboSingletons.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/dubboapache/BizFieldTaggingFilter.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/biz/BizFieldExtractor.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/monitorlog/MonitorLogTagger.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdAdvice.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/SetLogIdAdvice.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/LogIdPuller.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/SpanTagger.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcBridge.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerAdvice.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MarkerCapture.java
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/JsonFlattener.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/EmergencySamplerProvider.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/ApolloEndpointResolver.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/EmergencySampler.java
- baofoo-otel/agent-extension/src/main/java/com/baofoo/otel/sampler/RatioWatcher.java
- upstream/opentelemetry-collector-contrib/processor/tailsamplingprocessor/config.go
- upstream/opentelemetry-collector-contrib/processor/tailsamplingprocessor/processor.go
- baofoo-otel/collector-patch/tailsamplingprocessor/policy_watcher.go
- upstream/opentelemetry-collector-contrib/confmap/provider/apolloprovider/provider.go
- baofoo-otel/config/apollo-tailsampling.yml
- baofoo-otel/logid-ext/build.gradle.kts
- baofoo-otel/agent-extension/gradle.properties
- baofoo-otel/collector-build/slim-builder.yaml
- baofoo-otel/collector-build/build-slim.sh
- baofoo-otel/collector-build/build-linux.bat
- baofoo-otel/collector-build/pack-prod-bx.sh
- baofoo-otel/deploy/start-app-otel.sh
- baofoo-otel/logid-ext/src/main/java/com/baofoo/otel/logid/MdcPutAdvice.java