Skywalking插件开发指南

此文章翻译了skywalking官方的Java-Plugin-Development-Guide.mdhtml

这篇文档主要介绍理解,开发和贡献插件java

概念

Span(跨度)

在分布式的链路追踪系统里面Span是一个重要而又广泛的概念。咱们能够从Google Dapper Paper OpenTracing学习span的相关知识git

Skywalking从2017年就支持OpenTracing和OpenTracing-Java API。咱们的Span概念和OpenTracing以及google的论文里面的概念很是相思。并且咱们也扩展了Span。github

这里有三中类型的Spanapache

1.1 EntrySpan后端

EntrySpan表明了一个服务提供者,也就是服务端。做为一个APM系统,咱们关注应用服务器。因此几乎全部的服务和MQ的消费端都是EntrySpan。tomcat

1.2 LocalSpan服务器

LocalSpan能够理解为一个和远程服务无关的普通的Java方法,且这个Java方法即不是MQ的生产者,也不是消费者,更不是一个HTTP服务的生产者和消费者。app

1.3 ExitSpan框架

ExitSpan表明服务的客户端或者MQ的生产者,在Skywalking的早期版本里面名字是LeafSpan。例如经过JDBC访问DB,从Redis或者Memcached读取数据都被归类为ExitSpan。

ContextCarrier(上下文载体)

为了实现分布式的链路追踪,跨进城的追踪须要被绑定,上下文环境须要跨进程传播,这就是ContextCarrier的职责。

一下步骤是在一个A->B的分布式调用中,如何去使用ContextCarrier

  1. 在客户端建立一个空的ContextCarrier

  2. 经过 ContextManager#createExitSpan 方法建立一个新的ExitSpan或者使用ContextManager#inject去初始化ContextCarrier

  3. 将全部的ContextCarrier的信息放入到head(Http head),attachments(Dobbo RPC框架)或者messages(Kafka)中。

  4. 经过服务调用,ContextCarrier传播到服务端。

  5. 在服务端能够经过heads/attachments/messages获取到ContextCarrier的全部信息。

  6. ContextManager#createEntrySpan方法会建立一个EntrySpan或者使用 ContextManager#extract方法来将客户端和服务器绑定到一块儿。

让咱们用Apache HTTPComponent client端插件和Tomcat7 server插件来演示一下。

  1. Apache 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 7 服务端插件

    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(上下文快照)

除了跨进程,跨线程也须要获得支持,由于异步执行(内存中的MQ)和批处理在Java中很常见。 跨进程和跨线程是类似的,由于它们都是关于传播上下文。 惟一的区别是,跨线程不须要序列化。

这是跨线程传播的三个步骤:

1.使用ContextManager#capture获取ContextSnapshot对象。 2.让子线程经过方法参数或由现有参数携带的任何方式访问ContextSnapshot 3.在子线程中使用ContextManager#continued

 

 

Core APIs(核心API)

ContextManager

ContextManager提供了全部主要的和几本的API。

  1. 建立EntrySpan

public static AbstractSpan createEntrySpan(String endpointName, ContextCarrier carrier)

建立EntrySpan经过操做名(好比服务名、URI等) 和 ContextCarrier

  1. 建立LocalSpan

public static AbstractSpan createLocalSpan(String endpointName)

经过操做名称(好比方法的全限定名等)建立LoalSpan

  1. 建立ExitSpan

public static AbstractSpan createExitSpan(String endpointName, ContextCarrier carrier, String remotePeer)

经过操做名(好比服务名、URI等)、new一个ContextCarrier 地址信息(好比ip+port,或者hostname+port)和建立ExitSpan

AbstractSpan

    /**
     * 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 endpointName);

除了operation nametagslogs还有两个属性须要设置,即componentlayer

SpanLayer是span的一种,有五种类型:

  1. UNKNOWN (default)

  2. DB

  3. RPC_FRAMEWORK(RPC框架,不是一般的HTTP)

  4. HTTP

  5. MQ

Component IDs被skywalking项目定义和保留。

对于component name/ID扩展,请遵循 Component library definition and extension 文档。

Advanced APIs

Async Span APIs(异步Span API)

Span中有一组高级API,这些API专用于异步方案。 当span的标签,日志,属性(包括结束时间)须要在另外一个线程中设置,则应使用这些API。

    /**
     * The span finish at current tracing context, but the current span is still alive, until {@link #asyncFinish}
     * called.
     *
     * This method must be called<br/>
     * 1. In original thread(tracing context).
     * 2. Current span is active span.
     *
     * During alive, tags, logs and attributes of the span could be changed, in any thread.
     *
     * The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.
     *
     * @return the current span
     */
    AbstractSpan prepareForAsync();

    /**
     * Notify the span, it could be finished.
     *
     * The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.
     *
     * @return the current span
     */
    AbstractSpan asyncFinish();
  1. 在原始上下文中调用#prepareForAsync

  2. 完成当前线程中的工做后,在原始上下文中执行ContextManager#stopSpan

  3. 将跨度传播到任何其余线程。

  4. 完成全部设置后,在任何线程中调用#asyncFinish

  5. 跟踪上下文将完成,并在全部跨度的#prepareForAsynsc完成时向后端报告(由API执行次数判断)。

 

Develop a plugin(开发插件)

Abstract

追踪的几本方法是使用字节码技术或者AOP来拦截Java方法。

Skywalking封装了字节码操做并追踪上下午传播,因此你只须要定义拦截点(在Spring中也称为切入点)便可。

Intercept

Skywalking提供了两种通用的拦截构造函数的方法:实例方法和类方法。

  • 扩展ClassInstanceMethodsEnhancePluginDefine类,定义Constructor拦截点和instance method拦截点。

  • 扩展ClassStaticMethodsEnhancePluginDefine定义类方法拦截点。

固然,你也能够扩展ClassEnhancePluginDefine,来设置全部的拦截点,可是通常不这么用。

Implement plugin

下面演示如何经过扩展ClassInstanceMethodsEnhancePluginDefine实现一个插件。

  1. 定义目标类名称

protected abstract ClassMatch enhanceClass();

ClassMatch表示如何匹配目标类,有四种方式:

  • byName,经过类的全限定名称(包名+ . + 类名

  • byClassAnnotationMath,经过类存在的特定注解

  • byMethodAnnotationMatch,经过类方法存在的特定注解

  • byHierarchyMatch,经过类的父类或者接口

 

注意:

  • 在加强定义中,永远不要使用ThirdPartyClass.class,好比takesArguments(ThirdPartyClass.class),或者 byName(ThirdPartyClass.class.getName()),由于在目标应用中并不必定存在ThirdPartyClass,且这会破坏Agent。 咱们在CI中有import检查来帮助校验,可是它并不涵盖此限制的全部状况,所以切勿尝试经过使用彻底限定的类名(FQCN)之类的方法来解决此限制,例如:takesArguments(full.qualified.ThirdPartyClass.class)byName(full.qualified.ThirdPartyClass.class.getName())将经过CI检查,可是在agent的代码中仍然无效,请使类的全限定名

  • 即便你彻底肯定要拦截的类也存在于目标应用程序中(例如JDK的类),仍然不要使用*.class.getName()来获取类的String名称,建议使符串。 这是为了不ClassLoader带来的问题。

  • by*AnnotationMatch 不支持继承的注解

  • 不推荐使用 byHierarchyMatch,除非必需要用的时候。由于可能会触发拦截许多不是本身想拦截的方法,这会致使性能问题。

例如:

@Override
protected ClassMatch enhanceClassName() {
    return byName("org.apache.catalina.core.StandardEngineValve");  
}        
  1. 定义一个实例方法的拦截点:

public 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();
}

还可使用Matcher设置目标方法。 若是想要在拦截器中修改参数引用,则须要在isOverrideArgs中返回true。

如下各章节将讲述如何实现拦截器。

3.将插件定义添加到skywalking-plugin.def文件中

tomcat-7.x/8.x=TomcatInstrumentation

Implement an interceptor

实现一个实例方法拦截器,须要实现接口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);
}

beforeafterexception环境使用这些核心API

 

启动类加强机制

SkyWalking已将引导程序方法打包在agent-core里面。 经过在Instrumentation定义,很容易启用。

重写方法 public boolean isBootstrapInstrumentation() 且返回true,以下所示:

public class URLInstrumentation extends ClassEnhancePluginDefine {
    private static String CLASS_NAME = "java.net.URL";

    @Override protected ClassMatch enhanceClass() {
        return byName(CLASS_NAME);
    }

    @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return new ConstructorInterceptPoint[] {
            new ConstructorInterceptPoint() {
                @Override public ElementMatcher<MethodDescription> getConstructorMatcher() {
                    return any();
                }

                @Override public String getConstructorInterceptor() {
                    return "org.apache.skywalking.apm.plugin.jre.httpurlconnection.Interceptor2";
                }
            }
        };
    }

    @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[0];
    }

    @Override public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
        return new StaticMethodsInterceptPoint[0];
    }

    @Override public boolean isBootstrapInstrumentation() {
        return true;
    }
}

注意,仅在必要时进行引导检测,但大多数状况下会影响JRE core(rt.jar),并可能意料以外的结果和反作用。

贡献插件到Apache SkyWalking仓库

咱们欢迎你们贡献插件。

请按照如下步骤操做:

  1. 提交一个有关您要贡献哪些插件的问题,包括支持的版本。

  2. apm-sniffer/apm-sdk-plugin 或者apm-sniffer/optional-plugins模块下建立自模块,插件项目的名称须要包含支持的库的名称和版本

  3. 按照本指南进行开发。 确保提供了注释和测试用例。

  4. 开发和测试。

  5. 提供自动测试用例。如何编写插件测试用例,能够参考此文档

  6. 发送pr并申请review

  7. 插件提交者审批经过提交的插件,插件CI-with-IT,e2e和插件测试经过。

  8. SkyWalking接受插件。

相关文章
相关标签/搜索