☰
ByteBuddy无侵入埋点:第三方SDK日志拦截与Agent全实战
2026/10/21 12:03:08 网站建设 项目流程

先说个实务场景。线上有个查询接口时不时超时,业务方连续反馈了三天,日志里却只有我们应用层打印的“请求已发出”,至于第三方 SDK 到底把请求发出去没有、回包是什么、卡在哪一步,全都是黑的。这个 SDK 就是典型黑盒:你调它的方法,它内部自己组包、签名、发请求、解析响应,每一步都可能出问题,但每一步都不留痕迹。最初想的是把二十多处调用点全部手工包一层日志,可 SDK 升级后方法名一变,整片代码又要重写,实在忍不了。后来我试了两套基于 ByteBuddy 的方案:一套是动态代理,适合能替换 SDK 客户端实例的场景;另一套是字节码增强(Java Agent),能做到真正无侵入拦截。这篇文章把完整实现和几个把生产环境搞挂过的坑一起写出来,适合正要接第三方 SDK 的 Java 后端开发,也适合对字节码埋点感兴趣的人。

1. 从一次“调了但没日志”的线上事故说起

1.1 事故还原:SDK 内部完全是个黑盒

当时现象很典型:某个定时任务每隔一段时间调用 SDK 的查询方法,偶发返回超时异常,但异常堆栈只到 SDK 内部某个HttpClient.execute,参数和响应报文一概看不到。推断是网络抖动,可没有日志背书,谁也不敢拍板。后来我们尝试在调用点加log.info("req={}", request),发现真正问题不在调用点,而在 SDK 内部做了好几层封装——某个超时配置在内部组件里被静默覆盖了。这件事让我意识到:第三方 SDK 接进来之后,第一步不是跑通业务,而是先想办法让它“说话”。

给 SDK 加日志这件事,难点不是“加”,而是“加在哪”。你只能看到自己写的调用代码,SDK 内部几十个类之间的互调完全无感。如果 SDK 还用了独立线程、异步回调、内部缓存,那问题就更麻烦,因为你根本不知道程序实际执行路径长什么样。

1.2 手工埋点、JDK 动态代理、CGLIB,为什么都不顺手

先列一下当时我考虑过的三个路线,以及它们各自的死穴:

方案需要改业务代码能拦 SDK 内部自调用能拦静态方法典型限制
手工封装门面是否只能重新封装工作量大,SDK 升级后要同步改
JDK 动态代理是,替换实例否否只支持接口,没法拦类
CGLIB 子类代理是,替换实例否否final 类、final 方法不行
ByteBuddy 动态代理是,替换实例否否比 CGLIB 灵活,但同样覆盖不了内部自调用
ByteBuddy Agent否是是需要理解字节码增强和类加载机制

手工门面最土,但最可控,缺点是一旦 SDK 升级或者新增方法,门面类就得跟着改,维护成本很高。JDK 动态代理只能对接口生成代理,很多 SDK 直接给实现类甚至抽象类,这条路直接堵死。CGLIB 能对类做子类代理,但 SDK 的客户端类一加 final 就完蛋。ByteBuddy 在动态代理这一层几乎兼容了前面两者的能力,还能优雅处理构造方法、静态方法——当然,这需要切到 Agent 路线才能真正发挥出来。

正是因为这些限制,我最后选择了 ByteBuddy 作为统一方案:能替换实例的场景用动态代理,不能替换实例或要拦截内部调用的场景直接上 Java Agent。两种模式可以互补,共用同一套日志记录逻辑。

2. ByteBuddy 动态代理:适合你能替换 SDK 客户端实例的场景

2.1 先分清 ByteBuddy 的“动态代理”到底做了什么

很多人一说到动态代理就想到java.lang.reflect.Proxy。ByteBuddy 做的其实是更底层的事情:在运行时生成一个目标类的子类(或者目标接口的实现类),然后把拦截逻辑通过MethodDelegation织入到指定方法上。和 JDK 代理相比,它不要求目标必须是接口,返回值类型推断也更灵活,代码写起来也更像“在一个类上定义拦截器”。

这种方案的使用前提是:SDK 允许你拿到它的客户端实例,并且你可以把它注册回去。比如很多 SDK 都是工厂模式创建一个 client,然后由你手动注入到业务服务里。这时候你就有机会在注入前替换成 ByteBuddy 生成的代理对象。只要外部调用都走这个代理,就能记录完整调用日志。

2.2 完整代码:生成代理、委托调用、记录日志

Maven 依赖先加上,版本选一个比较新的稳定版:

<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.14.17</version> </dependency>

这里只需要byte-buddy,如果后面要动态 attach 再加byte-buddy-agent。

假设 SDK 给了这么个接口:

public interface DemoPayClient { PayResult createOrder(CreateOrderRequest request); QueryResult queryOrder(String orderId); }

我们生成一个代理实现,把所有方法调用委托给原始实例,同时记录日志:

import net.bytebuddy.ByteBuddy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ByteBuddyProxyFactory { /** * 为接口生成代理,所有方法调用都会被 DelegateInterceptor 记录。 */ public static <T> T wrap(Class<T> iface, T delegate) { Class<? extends T> proxyType = new ByteBuddy() .subclass(iface) .method(ElementMatchers.isDeclaredBy(iface)) .intercept(MethodDelegation.to(new DelegateInterceptor<>(delegate))) .make() .load(iface.getClassLoader()) .getLoaded(); try { return proxyType.getDeclaredConstructor().newInstance(); } catch (ReflectiveOperationException e) { throw new IllegalStateException("生成动态代理失败", e); } } }

拦截器核心逻辑:

import net.bytebuddy.implementation.bind.annotation.AllArguments; import net.bytebuddy.implementation.bind.annotation.Origin; import net.bytebuddy.implementation.bind.annotation.RuntimeType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class DelegateInterceptor<T> { private final T delegate; public DelegateInterceptor(T delegate) { this.delegate = delegate; } @RuntimeType public Object intercept(@Origin Method method, @AllArguments Object[] args) throws Exception { long start = System.currentTimeMillis(); try { Object result = method.invoke(delegate, args); SdkCallLogger.log("proxy", method.getName(), args, result, System.currentTimeMillis() - start, null); return result; } catch (InvocationTargetException e) { SdkCallLogger.log("proxy", method.getName(), args, null, System.currentTimeMillis() - start, e.getCause()); throw e.getCause(); } } }

而SdkCallLogger在第四章会专门讲。这套代码跑起来之后,调用方拿到的DemoPayClient其实是代理对象,业务代码完全无感。日志里可以看到每次调用的方法名、参数、返回结果、耗时和异常信息。

2.3 为什么这个方案没法用在最脏的地方

动态代理看起来很美好,但它有三个硬伤。

第一,SDK 内部如果存在自调用,比如createOrder内部又调用了自己的queryOrder,这个内部调用不会经过代理对象,日志会漏掉一层。因为代理对象只是一个实现了同一接口的新对象,SDK 内部持有的引用还是原始实例或者它自己创建的对象,你根本替换不了。

第二,如果 SDK 的关键能力是通过静态方法暴露的,比如DemoSdk.sign(data),动态代理完全拦不了。静态方法不依赖实例,没有“替换实例”这个操作空间。

第三,有些 SDK 客户端类是 final 的,或者构造方法私有。subclass会生成子类,遇到 final 类直接失败;遇到私有构造方法,你得额外配置构造方法的拦截逻辑,代码复杂度上一个台阶。

所以动态代理方案我只推荐在“客户端实例允许自己注入”且“主要调用路径都走这个实例”的场景用。如果 SDK 内部黑得厉害,直接跳到第三章的 Java Agent 方案,那才是真正意义上的兜底。

3. 核心方案:用 ByteBuddy Agent 做字节码级拦截

3.1 Java Agent 的原理一句话讲清楚

Java Agent 本质上就是一个带premain或agentmain方法的普通 jar,JVM 启动时通过-javaagent参数加载它,然后用Instrumentation把字节码转换器登记进去。ByteBuddy 的AgentBuilder封装了这套流程,我们只需要声明“哪些类的哪些方法要改”和“改成什么样”。关键点在于:增强发生在类加载阶段,SDK 内部后续无论怎么自调用,只要它加载的是被修改过的类,拦截就会生效。这是动态代理方案没法做到的。

这也是为什么我说它是“兜底方案”:你不需要替换任何实例,不需要改业务代码,只要目标类被加载,逻辑就被织入。

3.2 Agent 工程的依赖与打包注意事项

Agent 不能直接塞进业务工程里,一般单独抽成一个模块,最终打出一个可独立执行的 jar。Maven 依赖要同时引入 ByteBuddy 和它自带的 agent 工具:

<dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.14.17</version> </dependency> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.14.17</version> </dependency>

打包时要在 Manifest 里指定Premain-Class(如果需要动态附加,再加Agent-Class),我用maven-shade-plugin最省事,它会自动把依赖合并进同一个 jar:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>com.demo.logagent.SdkLogAgent</Premain-Class> <Agent-Class>com.demo.logagent.SdkLogAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin>

一个需要注意的打包细节:Agent jar 里尽量别依赖宿主应用自己的日志框架实现。原因在第六章类加载器那个坑里细说,简单讲就是“不要让你的拦截代码去引用宿主里才有的类”。

3.3 拦截规则与 Advice 代码

核心类长这样:

import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; import java.lang.instrument.Instrumentation; public class SdkLogAgent { public static void premain(String agentArgs, Instrumentation inst) { String[] parts = agentArgs == null ? new String[0] : agentArgs.split(","); final String typePattern = parts.length > 0 ? parts[0] : "com.example.sdk"; final String methodPattern = parts.length > 1 ? parts[1] : "pay"; new AgentBuilder.Default() .disableClassFormatChanges() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .type(ElementMatchers.nameStartsWith(typePattern)) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.nameStartsWith(methodPattern)) .intercept(Advice.to(SdkAdvice.class))) .installOn(inst); } }

Agent 的入口参数我用逗号分隔:第一个是类名前缀,第二个是方法名前缀。比如启动时传-javaagent:agent.jar=com.example.sdk,pay,就会拦截com.example.sdk包下所有以pay开头的方法。你可以改成精确匹配ElementMatchers.named("xxx"),或者用正则ElementMatchers.matches(".*(pay|query).*"),但匹配范围越宽,日志量和风险越大,后面会专门讲。

SdkAdvice用 ByteBuddy 的 Advice 模式织入方法入口和出口:

import net.bytebuddy.asm.Advice; public class SdkAdvice { @Advice.OnMethodEnter public static long enter(@Advice.Origin("#t.#m") String method, @Advice.AllArguments Object[] args) { SdkCallLogger.logBefore(method, args); return System.currentTimeMillis(); } @Advice.OnMethodExit(onThrowable = Throwable.class) public static void exit(@Advice.Origin("#t.#m") String method, @Advice.Enter long startTime, @Advice.Return Object result, @Advice.Thrown Throwable throwable) { SdkCallLogger.logAfter(method, result, throwable, System.currentTimeMillis() - startTime); } }

@Advice.Origin("#t.#m")会把被拦截方法的类名和方法名拼成字符串,日志里直接能看到来源。@Advice.Enter把enter方法返回的毫秒时间戳传给exit,这是 Advice 里跨方法传值的标准姿势。

3.4 为什么不直接用 MethodDelegation,而选 Advice

动态代理那节用的是MethodDelegation,它是把方法调用“委托”给某个拦截器对象;而Advice是直接把字节码以类似 try-finally 的方式内联到目标方法里。区别很关键:

MethodDelegation会生成一个具体的处理对象,调用时数值跳转到该对象的方法上,逻辑清晰,但性能略多一层调用。Advice则把enter / exit的代码直接复制到目标方法的入口和出口,没有额外对象和调用链,性能开销极低,适合高并发场景。

另外一个原因是栈信息。MethodDelegation会在异常堆栈里多插入一个拦截器方法的栈帧,线上看日志时容易干扰判断;Advice是内联的,堆栈几乎不会被“污染”。所以 Agent 场景我统一用Advice,动态代理场景才用MethodDelegation。

4. 日志记录的细节比拦截方法本身更重要

4.1 日志结构设计

拦截到方法调用之后,日志记录才是真正影响排查体验的地方。我最终把每条调用日志都做成一行 JSON,字段包含:方法名、入参、出参、耗时、异常类、异常信息、采样标记、时间戳。这样后续不管是写到文件、推到日志平台还是拿 grep 检索,都方便。

SdkCallLogger的核心实现:

import java.util.Arrays; import java.util.stream.Collectors; public final class SdkCallLogger { private static final ThreadLocal<Boolean> GUARD = ThreadLocal.withInitial(() -> Boolean.FALSE); private SdkCallLogger() { } public static void logBefore(String method, Object[] args) { if (GUARD.get()) return; GUARD.set(Boolean.TRUE); try { String argStr = Arrays.stream(args) .map(SdkCallLogger::toJson) .collect(Collectors.joining(" | ")); System.out.println("[SDK-LOG] enter method=" + method + " args=" + truncate(argStr, 2000)); } finally { GUARD.remove(); } } public static void logAfter(String method, Object result, Throwable throwable, long costMs) { if (GUARD.get()) return; GUARD.set(Boolean.TRUE); try { String line = String.format( "[SDK-LOG] exit method=%s cost=%dms result=%s throwable=%s", method, costMs, toJson(result), throwable == null ? "null" : throwable.getClass().getName() + ":" + throwable.getMessage()); System.out.println(truncate(line, 4000)); } finally { GUARD.remove(); } } }

toJson 我这里用的是 Jackson 序列化,代理/Advice 代码里只依赖 JDK 的类(System.out、String.format),具体序列化工具在 agent jar 里一起打进去。这里先不展开 toJson 的细节,因为序列化本身有一个大坑,放到第六章。

强调一下GUARD这个 ThreadLocal。它在logBefore开始处置为 TRUE,logAfter结束处 remove。这个设计是为了防止无限递归——比如 SDK 方法内部又调用了另一个被拦截的 SDK 方法,或者序列化入参时恰好触发了某个被拦截的方法。有了守卫,日志模块自身产生的任何调用都不会再次进入日志记录逻辑。

4.2 脱敏:签名、密钥、令牌不能落到明文日志

给 SDK 打日志很容易好心办坏事。很多 SDK 方法入参里带着签名、appSecret、token 这类字段,直接序列化成 JSON 写进日志,等于把密钥泄露到了日志系统里。日志平台权限一旦没控好,后果很严重。所以在 toJson 之后还要过一层脱敏:

import java.util.regex.Pattern; public final class MaskUtils { private static final Pattern SENSITIVE_FIELD = Pattern.compile( "\"((?i)sign|signature|secret|token|password|key)\"\\s*:\\s*\"[^\"]*\"" ); private MaskUtils() { } public static String mask(String json) { return SENSITIVE_FIELD.matcher(json) .replaceAll("\"$1\":\"***\""); } }

这个正则是示例级的,覆盖不了所有嵌套结构,但足以处理大多数 SDK 请求体的一层 JSON。如果你的 SDK 参数结构更深,可以换成基于字段名的递归脱敏,或者直接用 Jackson 的@JsonIgnore那是另一套思路了。我这里的原则是:宁可把字段整个打码,也不要赌日志平台没有外部访问风险。

4.3 截断、采样与 begin/end 对齐

日志过长会拖垮磁盘和检索。入参出参我在输出前统一截断,truncate实现大概是:

public static String truncate(String text, int maxLen) { if (text == null) return "null"; if (text.length() <= maxLen) return text; return text.substring(0, maxLen) + "...(" + text.length() + " chars omitted)"; }

采样这件事特别容易踩坑。如果一个方法调用量是每秒上万次,每行日志都完整写下来,日志文件一天几个 GB 很正常。所以我在logBefore里加了采样开关,但注意采样必须在 begin/end 两端保持一致。我的做法是:在logBefore里用 ThreadLocal 记录本次是否需要记完整日志,logAfter读取同一个标记决定是否输出。否则就会出现“只有 enter 没有 exit”的半截日志,排查时反而更困惑。

private static final double SAMPLE_RATE = Double.parseDouble(System.getProperty("sdk.log.sampleRate", "1.0")); private static final ThreadLocal<Boolean> SHOULD_LOG = ThreadLocal.withInitial(() -> Boolean.FALSE); public static void logBefore(String method, Object[] args) { if (ThreadLocalRandom.current().nextDouble() < SAMPLE_RATE) { SHOULD_LOG.set(Boolean.TRUE); // 正常记录 } else { SHOULD_LOG.set(Boolean.FALSE); } } public static void logAfter(String method, Object result, Throwable throwable, long costMs) { if (!Boolean.TRUE.equals(SHOULD_LOG.get())) { return; } SHOULD_LOG.remove(); // 正常记录 }

这样调整之后,sampleRate=0.1时只有 10% 的调用会成对记录,其余直接忽略。排查重大问题时把采样率调回 1.0 即可,方便灵活。

5. 线上服务不想重启?用 agentmain 动态附加

5.1 agentmain + VirtualMachine.attach 组合

有些服务不是你想重启就能重启的。premain 方案只对“启动时就带着 agent”的场景友好,已经运行起来的 JVM 需要另一条路:通过agentmain动态挂载。

Agent 的agentmain入口和premain写在一起,安装逻辑复用:

public static void agentmain(String agentArgs, Instrumentation inst) { String[] parts = agentArgs == null ? new String[0] : agentArgs.split(","); final String typePattern = parts.length > 0 ? parts[0] : "com.example.sdk"; final String methodPattern = parts.length > 1 ? parts[1] : "pay"; new AgentBuilder.Default() .disableClassFormatChanges() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .type(ElementMatchers.nameStartsWith(typePattern)) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.nameStartsWith(methodPattern)) .intercept(Advice.to(SdkAdvice.class))) .installOn(inst); // 对已经加载的类做一次重转换,让增强立刻生效 for (Class<?> clazz : inst.getAllLoadedClasses()) { if (clazz.getName().startsWith(typePattern) && inst.isModifiableClass(clazz)) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { System.err.println("retransform skipped: " + clazz.getName()); } } } }

然后外部用VirtualMachine把 agent jar 加载进目标进程:

import com.sun.tools.attach.VirtualMachine; public final class AttachSdkLogger { public static void attach(String pid, String agentJarPath) throws Exception { VirtualMachine vm = VirtualMachine.attach(pid); try { vm.loadAgent(agentJarPath, "com.example.sdk,pay"); } finally { vm.detach(); } } public static void main(String[] args) throws Exception { // 用法:java AttachSdkLogger <pid> <agent.jar> attach(args[0], args[1]); } }

JDK 9 以上如果遇到模块相关异常,编译或运行时记得加上--add-modules jdk.attach。正常打包出来的 JDK 自带这个模块,只是默认不在根模块集合里,手动引用时需要显式打开。

5.2 self-attach 的坑和 ByteBuddy 工具简化

一个隐蔽的坑是“给自己挂 agent”。如果业务进程和 attach 工具跑在同一个 JVM 里,比如通过一个运维接口触发动态挂载,JDK 9 以上默认禁止 self-attach,会抛IOException。解决办法是在目标 JVM 启动参数里加-Djdk.attach.allowAttachSelf=true。这个参数不是 JDK 的正式公共参数,不同版本表现不太一样,但在我实测的 JDK 11 上,加了这个才能真正把自己附加上去。

如果不希望手写这些兼容逻辑,byte-buddy-agent里提供了封装好的工具类,底层处理了不同 JDK 的 attach 细节。核心思路和上面的代码一致:找到目标进程、加载 agent jar、传递参数、分离。如果团队里运维同学不想关心这些底层机制,封装成一个命令行工具最稳妥。

动态附加有一个天然约束:只能对“可以被 retransform”的类生效。如果目标类被某个安全框架锁定了,或者 JVM 本身不支持类重转换(启动时没开相关指令),会直接失败。所以我在代码里做了isModifiableClass判断,遇到不可修改的类就跳过并打印警告,不至于让整条附加链路崩溃。

6. 生产环境实测中的 5 个坑(每一个都让程序出过问题)

6.1 无限递归:日志模块自己踩了被拦截的方法

第一次把 agent 装到测试环境,JVM 直接 StackOverflowError。原因很简单:某个 SDK 方法内部调用了另一个 SDK 方法,而我的logBefore在记录方法名时又通过method.toString()间接调用了某些被拦截的路径,日志模块自己再次触发了拦截。这个递归设计得特别隐蔽,不加守卫根本没发现。后来我在SdkCallLogger里加了 ThreadLocal 守卫,任何日志记录内部的调用都直接短路。这也是为什么我在第四章反复强调GUARD不能省。

6.2 重复增强:premain 和 agentmain 都进来了一次

一次线上事故里,同一方法被打印了两份日志,入参时间戳不同但方法名完全一样。原因是服务启动时带了 premain,中途运维又通过 agentmain 附加了一个相同的 agent jar,同一个目标方法被增强了两遍。解决方法是在 Agent 里维护一个全局安装标记,确保同一 JVM 只安装一次:

private static final AtomicBoolean INSTALLED = new AtomicBoolean(false); public static void installOnce(String agentArgs, Instrumentation inst) { if (!INSTALLED.compareAndSet(false, true)) { System.out.println("[SDK-LOG] agent already installed, skip."); return; } // 真正的安装逻辑 }

premain 和 agentmain 入口都先走installOnce,重复加载直接被拦下。

6.3 类加载器不一致:Advice 里引用宿主类导致 NoClassDefFoundError

用 Agent 做增强时,目标类可能是由应用程序自己的类加载器加载的,而 agent jar 的类加载路径往往不同。有一次我的SdkAdvice里直接引用了宿主工程的 Logger 工厂类,结果目标类在字节码增强时找不到这个类,直接抛NoClassDefFoundError,把整个调用链打崩。解决原则是:Advice 相关的代码只依赖 JDK 标准库,自己引用的工具类也必须打进 agent jar,并且不要通过宿主的类加载器去拿。日志输出用System.out或者java.util.logging最安全,虽然丑,但稳定。

6.4 序列化把 SDK 内部对象拉开一场连锁反应

这是最阴间的一个坑。我在toJson里对出参对象做反射序列化,结果那个对象内部某个字段是懒加载的,序列化时的getter触发了 SDK 内部的网络请求,而那个请求又恰好匹配了拦截规则。于是日志系统在“记录一次调用”的过程中,间接引发了一次全新的 SDK 调用,整个链路被拉长了好几倍。后来我把“序列化所有字段”改成了“只序列化指定的少量字段”,并且对可疑对象直接走toString摘要而不做全量反射。这里想强调:打日志本身也是有副作用的,不要以为 log 就绝对安全。

6.5 匹配范围过宽导致日志爆炸

刚开始图省事,类型匹配直接写ElementMatchers.nameStartsWith("com.example.sdk"),结果 SDK 包底下负责 HTTP 传输的内部类也被拦了。一个业务请求进来,日志里出现几十行 SDK 内部组包、发请求、解析响应的记录,排障时反而分不清主次。这个问题的教训是:匹配规则要尽量精确。先只针对你关心的入口方法名(比如createOrder、queryOrder),确定有效后再逐步放宽。日志记录也一样,线上环境采样率先调到 0.01 观察,确认没影响再调高,不要一上来就 1.0 全量记录。

最后分享一点我自己的使用习惯

这套 ByteBuddy 埋点方案用了大半年,我现在接任何一个新的第三方 SDK,第一件事不是写业务代码,而是先把 SDK 的调用面理一遍:哪些方法最需要日志、哪些参数必须脱敏、这个方法有没有可能被高频调用。用 Agent 方式拦截时,我通常会先预置一个“只拦入口方法 + 采样率 0.01”的配置上线跑一天,确认稳定后再逐步放开。日志是给人看的,不是给机器刷的,记录得太多跟没记录一样难排查。如果你也想给自己的项目做无侵入调用日志,可以先从动态代理方案写起,跑通之后再切到 Agent,这样理解起来会更顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询