skywalking是使用字节码操作技术和AOP概念拦截Java类方法的方式来追踪链路的,由于skywalking已经打包了字节码操作技术和链路追踪的上下文传播,因此只需定义拦截点即可。
这里以skywalking-8.7.0版本为例。
关于插件拦截的原理,可以看我的另一篇文章:skywalking插件工作原理剖析
1. 创建插件模块
在 apm-sniffer/apm-sdk-plugin
目录下创建一个插件maven子模块。
2. 插件开发(1)思路
- 定义拦截点,通常是类的方法
- 定义拦截器,支持在拦截方法执行前后进行日志采集
- 定义配置文件,启用拦截点
- 编译打包,将生成的jar放到探针的plugins目录下
(2)定义拦截点① 官方提供的拦截点扩展入口
skywalking提供了2种供扩展的拦截点:
- ClassInstanceMethodsEnhancePluginDefine:支持定义构造方法和实例方法的拦截点。
- ClassStaticMethodsEnhancePluginDefine:支持定义静态方法的拦截点。
当然还可以直接扩展
ClassEnhancePluginDefine
,这个类是上面两个类的父类。这种方式较为麻烦,一般不推荐使用。这里以拦截实例方法为例,继承
ClassInstanceMethodsEnhancePluginDefine
类。
② 类拦截规则
skywalking提供了4种类拦截的规则:
- byName:类名匹配(包名+类名)
- byClassAnnotationMatch:类注解匹配
- byMethodAnnotationMatch:方法注解匹配
- byHierarchyMatch:父类或接口匹配
注意:
- 这里的匹配规则要用字符串,不要用类引用的方式(byName(ThirdPartyClass.class.getName())),否则可能会导致探针异常。
- 注解匹配的方式,不支持继承的注解
- 父类或接口匹配的方法,尽量避免使用,否则可能会出现一些难以预料的问题
③ 设置要拦截的类名
实现 enhanceClass()
方法,定义要拦截的类名,必须是全路径的名称,即包名+类名。
④ 设置拦截的实例方法和拦截器的类名
实现 getInstanceMethodsInterceptPoints()
方法,定义要拦截的实例方法,以及对应拦截器的类名。拦截器类名也是包名+类名。
这里支持定义多个实例方法,每个实例方法可以使用不同的拦截器。还支持拦截私有方法(private)。
⑤ 代码示例
下面的代码实现的功能是:使用拦截器
MingBaoServiceInterceptor
拦截MingBaoService
类的service
方法。
public class MingBaoServiceInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { // 要拦截的类 private static final String ENHANCE_CLASS = "com.mingbao.service.MingBaoService"; // 拦截器的类名 private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.mingbao.service.MingBaoServiceInterceptor"; /** * 定义要拦截的类名 */ @Override protected ClassMatch enhanceClass() { return NameMatch.byName(ENHANCE_CLASS); } /** * 定义要拦截类的方法,以及对应的拦截器 */ @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { return new InstanceMethodsInterceptPoint[] { new InstanceMethodsInterceptPoint() { @Override public ElementMatcher getMethodsMatcher() { // 这里是要拦截的方法 return named("service"); } @Override public String getMethodsInterceptor() { // 定义拦截器的类名 return INTERCEPT_CLASS; } @Override public boolean isOverrideArgs() { // 如果有要改方法参数的需求,这里可以设置成true return false; } } }; } /** * 这里是拦截构造方法,忽略 */ @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { return null; }}
(3)定义拦截器① 3种常见的拦截器接口
- InstanceMethodsAroundInterceptor:实例方法拦截器
- StaticMethodsAroundInterceptor:静态方法拦截器
- InstanceConstructorInterceptor:构造方法拦截器
要拦截对应的方法,必须要实现对应的接口。这里以实现
InstanceMethodsAroundInterceptor
接口,拦截实例方法为例。
② 在方法执行前拦截
实现 beforeMethod(EnhancedInstance, Method, Object[], Class[], MethodInterceptResult)
方法,此方法会在被拦截的方法执行前执行。此方法中一般是定义日志链路节点span对象,一个span对象对应着日志链路中的一个节点。
- 拦截方法参数说明:
1.EnhancedInstance objInst:被增强的实例,一般用不上2.Method method:被拦截的方法3.Object[] allArguments:被拦截方法的入参4.Class[] argumentsTypes:被拦截方法的入参的类型5.MethodInterceptResult result:此参数可以作为被拦截方法的返回参数,如果给此参数赋值了,会阻断被拦截方法的执行,直接返回此参数。可以通过defineReturnValue()方法来定义要返回的数据。
- 链路节点对象span的类型:
节点对象都实现了
AbstractSpan
接口,可以借助ContextManager类来创建和获取节点对象。
创建一个span对象后,就会生成一个链路日志的节点。
1.EntrySpan:入口层span,它会作为一条链路的起点。如接收Http请求的接口层、Dubbo服务的提供方以及MQ消费者。2.LocalSpan:中间层span,它会出现在链路的中间节点上。如一个业务方法被调用。3.ExitSpan:出口层span,它会作为一条链路的终点。如发送Http请求的工具、Dubbo服务的调用方以及MQ生产者。
- 链路节点对象span常用的设置项
1.component:组件类型,比如说Tomcat、Dubbo、SpringMVC...可以从ComponentsDefine类中定义好的一些官方组件类型中选,自定义的组件类型是无法在UI中显示出来的。也可以不设置值,默认会显示Unknown。(可选的类型就那么多,一般自定义时根本找不到合适的)。2.layer:日志层级,可以从SpanLayer类中选择,一共就5个:DB、RPC_FRAMEWORK、HTTP、MQ和CACHE。可选的也不多,不合适可以不设置,默认会显示Unknown。3.tag:日志标签,支持自定义日志字段,可以通过 span.tag(new StringTag("msg"), msg) 的方式来设置。结合后端配置项 core.default.searchableTracesTags可以达到自定义字段搜索的目的。
③ 在方法执行后拦截
实现 afterMethod(EnhancedInstance, Method, Object[], Class[], Object)
方法,此方法会在被拦截的方法执行前执行。此方法中一般是将方法的返回数据记录到链路节点对象中。
- 拦截方法参数说明:
前4个参数和beforeMethod()方法中一样,介绍下最后那个参数Object ret:方法的返回数据
④ 代码示例
public class MingBaoServiceInterceptor implements InstanceMethodsAroundInterceptor { private static final Gson GSON = new Gson(); /** * 在拦截方法前执行 */ @Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { // 解析方法入参 String message = (String) allArguments[0]; // 初始化span,这里创建了一个EntrySpan ContextCarrier contextCarrier = new ContextCarrier(); AbstractSpan span = ContextManager.createEntrySpan(MethodUtil.generateOperationName(method), contextCarrier); // 自定义标签,记录方法入参 span.tag(new StringTag("req"), req); // 下面的参数如果不合适可以不设置 span.setLayer(SpanLayer.MQ); span.setComponent(new OfficialComponent(999, "mbService")); } /** * 在拦截方法后执行 */ @Override public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) throws Throwable { // 获取上下文中的span对象 AbstractSpan span = ContextManager.activeSpan(); // 自定义标签,记录方法出参 span.tag(new StringTag("resp"), GSON.toJson(ret)); // 停止日志记录,移除上下文 ContextManager.stopSpan(); // 返回方法出参 return ret; } /** * 记录方法异常 */ @Override public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { ContextManager.activeSpan().log(t); }}
(4)定义配置文件
在自定义插件模块的resources目录下定义 skywalking-plugin.def
配置文件,该文件用于帮助探针启动时加载插件时,寻找插件拦截点。
注意:自定义插件时,不管是类还是配置文件,都要把apache的许可证注释带上,可以参考其他插件类文件中最上面被注释的那一段。
mingbao-service=org.apache.skywalking.apm.plugin.mingbao.service.MingBaoServiceInstrumentation
3. 使用插件(1)插件打包
对自定义的插件子模块执行mvn package操作,构建完成后会生成一个名称类似 mingbao-service-plugin-8.7.0.jar
的jar包,将jar包拷贝到 skywalking-agent/plugins
目录下。
(2)使用自定义插件
自定义插件的使用和自带插件使用方式相同,将
skywalking-agent
打包到项目镜像中,使用javaagent探针启动即可。
这里有个建议:如果使用docker来部署项目,可以将 skywalking-agent
目录放到项目同级目录下,并在项目同级目录下构建docker镜像。因为docker build命令无法操作命令执行目录的父级目录所包含的其他文件。因此要保证 skywalking-agent
要在执行docker build命令的目录下。
4. 检查插件生效
启动项目后,调用被拦截的类方法,然后看UI上是否生成了对应的日志。一般情况UI上会延迟几秒钟才会生成日志。
5. 可能会遇到的问题(1)插件不生效
在探针的logs目录下(docker镜像部署的项目要先进入镜像才能看到),会生成 skywalking-api.log
目录,日志默认级别为 INFO
。插件不生效时,一般情况下,日志文件中一定有错误日志。
- SecurityException
如果遇到报错:java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
,那么一般是因为自定义插件中依赖了第三方依赖包,在打包时生成了 *.SF
或 *.RSA
文件,把上述文件删掉即可。可以使用下面的命令:
zip -d mingbao-service-plugin-8.7.0.jar 'META-INF/*SF' 'META-INF/*RSA'