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接受插件。

相關文章
相關標籤/搜索