# 插件开发指南
这边文档描述插件的开发和贡献方法
# 核心概念
# 一. Span
Span是追踪系统中的通用概念(有时候被翻译成埋点),关于Span的定义,请参考OpenTracing 中文版。
SkyWalking作为OpenTracing的支持者,在核心实现中,与标准有较高的相似度。当然,作为实际产品的需要,我们一会扩展相关概念。
我们将span分为三类:
1.1 EntrySpan EntrySpan代表一个服务的提供方,即,服务端的入口点。它是每个Java对外服务的入口点。如:Web服务入口就是一个EntrySpan。
1.2 LocalSpan LocalSpan代表一个普通的Span,代表任意一个本地逻辑块(或方法)
1.3 ExitSpan ExitSpan也可以称为LeafSpan(SkyWalking的早期版本中的称呼),代表了一个远程服务的客户端调用。如:一次JDBC调用。
# 二. ContextCarrier
分布式追踪要解决的一个重要问题,就是跨进程调用链连接的问题,ContextCarrier的概念就是为了解决这种场景。
当发生一次A->B的网络调用时:
- 创建一个空的ContextCarrier
- 通过
ContextManager#createExitSpan
方法创建一个ExitSpan,或者使用ContextManager#inject
,在过程中传入并初始化ContextCarrier
- 将
ContextCarrier
中所有元素放入请求头(如:HTTP头)或消息正文(如 Kafka) ContextCarrier
随请求传输到服务端- 服务端收到后,转换为新的ContextCarrier
- 通过
ContestManager#createEntrySpan
方法创建EntrySpan,或者使用ContextManager#extract
,建立分布式调用关联
以HTTPComponent调用Tomcat为例:
- 客户端(HTTPComponent端)
span = ContextManager.createExitSpan("/span/operation/name", contextCarrier, "ip:port");
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());
}
- 服务端(Tomcat端)
ContextCarrier contextCarrier = new ContextCarrier();
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
next.setHeadValue(request.getHeader(next.getHeadKey()));
}
span = ContextManager.createEntrySpan(“/span/operation/name”, contextCarrier);
# 三. ContextSnapshot
除了跨进程的RPC调用,另外一种追踪的常见场景是跨线程保持链路连接。跨线程和跨进程有很高的相似度,都是需要完成上下文的传递工作。 所以ContextSnapshot具有和ContextCarrier十分类似的API风格。
当发生一次A->B的跨线程调用时:
- 需要在A线程中通过ContextManager#capture操作生成ContextSnapshot对象实例
- 将这个ContextSnapshot对象传递到B线程中
- B线程通过ContextManager#continued操作完成上下文传递
# 核心API
# 一. ContextManager
ContextManager提供了追踪相关操作的主入口
- 创建EntrySpan
public static AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier)
通过服务名、跨进程传递的ContextCarrier,创建EntrySpan。
- 创建LocalSpan
public static AbstractSpan createLocalSpan(String operationName)
根据服务名(或方法名),创建LocalSpan
- 创建ExitSpan
public static AbstractSpan createExitSpan(String operationName, ContextCarrier carrier, String remotePeer)
根据服务名,跨进程传递的ContextCarrier(空容器)和远端服务地址(IP、主机名、域名 + 端口),创建ExitSpan
# 二. AbstractSpan
AbstractSpan提供了Span内部,进行操作的各项API
/**
* Set the component id, which defines in {@link ComponentsDefine}
*
* @param component
* @return the span for chaining.
*/
AbstractSpan setComponent(Component component);
/**
* Only use this method in explicit instrumentation, like opentracing-skywalking-bridge.
* It it higher recommend don't use this for performance consideration.
*
* @param componentName
* @return the span for chaining.
*/
AbstractSpan setComponent(String componentName);
AbstractSpan setLayer(SpanLayer layer);
/**
* Set a key:value tag on the Span.
*
* @return this Span instance, for chaining
*/
AbstractSpan tag(String key, String value);
/**
* Record an exception event of the current walltime timestamp.
*
* @param t any subclass of {@link Throwable}, which occurs in this span.
* @return the Span, for chaining
*/
AbstractSpan log(Throwable t);
AbstractSpan errorOccurred();
/**
* Record an event at a specific timestamp.
*
* @param timestamp The explicit timestamp for the log record.
* @param event the events
* @return the Span, for chaining
*/
AbstractSpan log(long timestamp, Map<String, ?> event);
/**
* Sets the string name for the logical operation this span represents.
*
* @return this Span instance, for chaining
*/
AbstractSpan setOperationName(String operationName);
Span的操作语义和OpenTracing类似。
SpanLayer为我们的特有概念,如果是远程调用类的服务,请设置此属性,包括5个属性值
- UNKNOWN, 默认
- DB
- RPC_FRAMEWORK,非HTTP类型的RPC框架,如:原生的DUBBO,MOTAN
- HTTP
- MQ
Component ID被SkyWalking项目组定义和保护。0到10000为保留值,如果你希望贡献新插件,可以在插件pull request通过,并提交的自动化 测试用户被接收后,申请自己的组件ID。私有插件,请使用10000以上的ID,避免重复。
# 开发插件
# 一. 简介
因为所有的程序调用都是基于方法的,所以插件实际上就是基于方法的拦截,类似面向切面编程的AOP技术。SkyWalking底层已经完成相关的技术封装,所以插件开发者只需要定位需要拦截的类、方法,然后结合上文中的追踪API,即可完成插件的开发。
# 二. 拦截类型
根据Java方法,共有三种拦截类型
- 拦截构造函数
- 拦截实例方法
- 拦截静态方法
我们将这三类拦截,分为两类,即:
- 实例方法增强插件,继承ClassInstanceMethodsEnhancePluginDefine
- 静态方法增强插件,继承ClassStaticMethodsEnhancePluginDefine
当然,也可以同时支持实例和静态方法,直接继承ClassEnhancePluginDefine。但是,这种情况很少。
# 三. 实现自己的插件定义
我们以继承ClassInstanceMethodsEnhancePluginDefine为例(ClassStaticMethodsEnhancePluginDefine十分类似,不再重复描述),描述定义插件的全过程
- 定义目标类名称
protected abstract ClassMatch enhanceClass();
ClassMatch反应类的匹配方式,目前提供四种:
- byName, 通过类名完整匹配
- byClassAnnotationMatch, 通过类标注进行匹配
- byMethodAnnotationMatch, 通过方法的标注来匹配类
- byHierarchyMatch, 通过父类或者接口匹配
注意实现:
- 所有类、接口、标注名称,请使用字符串,不要使用
*.class.getName()
(用户环境可能会引起ClassLoader问题)。 - by*AnnotationMatch不支持继承的标注
- byHierarchyMatch,如果存在接口、抽象类、类间的多层继承关系,如果方法复写,则可能造成多层埋点。
如:
@Override
protected ClassMatch enhanceClassName() {
return byName("org.apache.catalina.core.StandardEngineValve");
}
- 定义方法拦截点
protected InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints();
public interface InstanceMethodsInterceptPoint {
/**
* class instance methods matcher.
*
* @return methods matcher
*/
ElementMatcher<MethodDescription> getMethodsMatcher();
/**
* @return represents a class name, the class instance must instanceof InstanceMethodsAroundInterceptor.
*/
String getMethodsInterceptor();
boolean isOverrideArgs();
}
返回拦截方法的匹配器,以及对应的拦截类,同样由于潜在的ClassLoader问题,不要使用*.class.getName()
。如何构建拦截器,请章节"四. 实现拦截器逻辑"。
- 定义skywalking-plugin.def文件
tomcat-7.x/8.x=TomcatInstrumentation
- 插件名称,要求全局唯一,命名规范:目标组件+版本号
- 插件定义类全名
# 四. 实现拦截器逻辑
我们继续以实现实例方法拦截为例,拦截器需要实现org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor。
/**
* A interceptor, which intercept method's invocation. The target methods will be defined in {@link
* ClassEnhancePluginDefine}'s subclass, most likely in {@link ClassInstanceMethodsEnhancePluginDefine}
*
* @author wusheng
*/
public interface InstanceMethodsAroundInterceptor {
/**
* called before target method invocation.
*
* @param result change this result, if you want to truncate the method.
* @throws Throwable
*/
void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable;
/**
* called after target method invocation. Even method's invocation triggers an exception.
*
* @param ret the method's original return value.
* @return the method's actual return value.
* @throws Throwable
*/
Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable;
/**
* called when occur exception.
*
* @param t the exception occur.
*/
void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Throwable t);
}
可以在方法执行前、执行后、执行异常三个点,进行拦截,设置修改方法参数(执行前),并调用核心API,设置追踪逻辑。
# 贡献插件到主仓库
我们鼓励大家共同贡献支持各个类库的插件。
大家需支持以下步骤执行:
- 在issue页面提出插件扩展需求,对应的版本。
- Fork apache/incubator-skywalking到本地
- 在apm-sniffer/apm-sdk-plugin下新建自己的插件模块,模块名为:支持类库名称+版本号
- 按照规范开发插件
- 完善注释和测试用例
- 在本地打包进行集成测试
- 提交Pull Request到 apache/incubator-skywalking,根据评审团队要求,提供相关自动化测试用例
- SkyWalking Committer成员完成插件审核,确定发布版本,并合并到主仓库。