# 插件开发指南

这边文档描述插件的开发和贡献方法

# 核心概念

# 一. 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的网络调用时:

  1. 创建一个空的ContextCarrier
  2. 通过ContextManager#createExitSpan方法创建一个ExitSpan,或者使用ContextManager#inject,在过程中传入并初始化ContextCarrier
  3. ContextCarrier中所有元素放入请求头(如:HTTP头)或消息正文(如 Kafka)
  4. ContextCarrier随请求传输到服务端
  5. 服务端收到后,转换为新的ContextCarrier
  6. 通过ContestManager#createEntrySpan方法创建EntrySpan,或者使用ContextManager#extract,建立分布式调用关联

以HTTPComponent调用Tomcat为例:

  1. 客户端(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());
            }
  1. 服务端(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的跨线程调用时:

  1. 需要在A线程中通过ContextManager#capture操作生成ContextSnapshot对象实例
  2. 将这个ContextSnapshot对象传递到B线程中
  3. B线程通过ContextManager#continued操作完成上下文传递

# 核心API

# 一. ContextManager

ContextManager提供了追踪相关操作的主入口

  1. 创建EntrySpan
public static AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier)

通过服务名、跨进程传递的ContextCarrier,创建EntrySpan。

  1. 创建LocalSpan
public static AbstractSpan createLocalSpan(String operationName)

根据服务名(或方法名),创建LocalSpan

  1. 创建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个属性值

  1. UNKNOWN, 默认
  2. DB
  3. RPC_FRAMEWORK,非HTTP类型的RPC框架,如:原生的DUBBO,MOTAN
  4. HTTP
  5. MQ

Component ID被SkyWalking项目组定义和保护。0到10000为保留值,如果你希望贡献新插件,可以在插件pull request通过,并提交的自动化 测试用户被接收后,申请自己的组件ID。私有插件,请使用10000以上的ID,避免重复。

# 开发插件

# 一. 简介

因为所有的程序调用都是基于方法的,所以插件实际上就是基于方法的拦截,类似面向切面编程的AOP技术。SkyWalking底层已经完成相关的技术封装,所以插件开发者只需要定位需要拦截的类、方法,然后结合上文中的追踪API,即可完成插件的开发。

# 二. 拦截类型

根据Java方法,共有三种拦截类型

  1. 拦截构造函数
  2. 拦截实例方法
  3. 拦截静态方法

我们将这三类拦截,分为两类,即:

  1. 实例方法增强插件,继承ClassInstanceMethodsEnhancePluginDefine
  2. 静态方法增强插件,继承ClassStaticMethodsEnhancePluginDefine

当然,也可以同时支持实例和静态方法,直接继承ClassEnhancePluginDefine。但是,这种情况很少。

# 三. 实现自己的插件定义

我们以继承ClassInstanceMethodsEnhancePluginDefine为例(ClassStaticMethodsEnhancePluginDefine十分类似,不再重复描述),描述定义插件的全过程

  1. 定义目标类名称
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");		
}		      

  1. 定义方法拦截点
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()。如何构建拦截器,请章节"四. 实现拦截器逻辑"。

  1. 定义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,设置追踪逻辑。

# 贡献插件到主仓库

我们鼓励大家共同贡献支持各个类库的插件。

大家需支持以下步骤执行:

  1. 在issue页面提出插件扩展需求,对应的版本。
  2. Fork apache/incubator-skywalking到本地
  3. 在apm-sniffer/apm-sdk-plugin下新建自己的插件模块,模块名为:支持类库名称+版本号
  4. 按照规范开发插件
  5. 完善注释和测试用例
  6. 在本地打包进行集成测试
  7. 提交Pull Request到 apache/incubator-skywalking,根据评审团队要求,提供相关自动化测试用例
  8. SkyWalking Committer成员完成插件审核,确定发布版本,并合并到主仓库。