此文章翻譯了skywalking官方的Java-Plugin-Development-Guide.mdhtml
這篇文檔主要介紹理解,開發和貢獻插件java
在分佈式的鏈路追蹤系統裏面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
的職責。
一下步驟是在一個A->B的分佈式調用中,如何去使用ContextCarrier
在客戶端建立一個空的ContextCarrier
。
經過 ContextManager#createExitSpan
方法建立一個新的ExitSpan
或者使用ContextManager#inject
去初始化ContextCarrier
。
將全部的ContextCarrier
的信息放入到head(Http head),attachments(Dobbo RPC框架)或者messages(Kafka)中。
經過服務調用,ContextCarrier
傳播到服務端。
在服務端能夠經過heads/attachments/messages獲取到ContextCarrier
的全部信息。
ContextManager#createEntrySpan
方法會建立一個EntrySpan
或者使用 ContextManager#extract
方法來將客戶端和服務器綁定到一塊兒。
讓咱們用Apache HTTPComponent client端插件和Tomcat7 server插件來演示一下。
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()); }
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);
除了跨進程,跨線程也須要獲得支持,由於異步執行(內存中的MQ)和批處理在Java中很常見。 跨進程和跨線程是類似的,由於它們都是關於傳播上下文。 惟一的區別是,跨線程不須要序列化。
這是跨線程傳播的三個步驟:
1.使用ContextManager#capture
獲取ContextSnapshot
對象。 2.讓子線程經過方法參數或由現有參數攜帶的任何方式訪問ContextSnapshot
。 3.在子線程中使用ContextManager#continued
。
ContextManager
提供了全部主要的和幾本的API。
建立EntrySpan
public static AbstractSpan createEntrySpan(String endpointName, ContextCarrier carrier)
建立EntrySpan經過操做名(好比服務名、URI等) 和 ContextCarrier
建立LocalSpan
public static AbstractSpan createLocalSpan(String endpointName)
經過操做名稱(好比方法的全限定名等)建立LoalSpan
建立ExitSpan
public static AbstractSpan createExitSpan(String endpointName, ContextCarrier carrier, String remotePeer)
經過操做名(好比服務名、URI等)、new一個ContextCarrier 地址信息(好比ip+port,或者hostname+port)和建立ExitSpan
/** * 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 name
、tags
、logs
還有兩個屬性須要設置,即component
和layer
SpanLayer
是span的一種,有五種類型:
UNKNOWN (default)
DB
RPC_FRAMEWORK(RPC框架,不是一般的HTTP)
HTTP
MQ
Component IDs被skywalking項目定義和保留。
對於component name/ID擴展,請遵循 Component library definition and extension 文檔。
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();
在原始上下文中調用#prepareForAsync
。
完成當前線程中的工做後,在原始上下文中執行ContextManager#stopSpan
。
將跨度傳播到任何其餘線程。
完成全部設置後,在任何線程中調用#asyncFinish
。
跟蹤上下文將完成,並在全部跨度的#prepareForAsynsc
完成時向後端報告(由API執行次數判斷)。
追蹤的幾本方法是使用字節碼技術或者AOP來攔截Java方法。
Skywalking封裝了字節碼操做並追蹤上下午傳播,因此你只須要定義攔截點(在Spring中也稱爲切入點)便可。
Skywalking提供了兩種通用的攔截構造函數的方法:實例方法和類方法。
擴展ClassInstanceMethodsEnhancePluginDefine
類,定義Constructor
攔截點和instance method
攔截點。
擴展ClassStaticMethodsEnhancePluginDefine
定義類方法攔截點。
固然,你也能夠擴展ClassEnhancePluginDefine
,來設置全部的攔截點,可是通常不這麼用。
下面演示如何經過擴展ClassInstanceMethodsEnhancePluginDefine
實現一個插件。
定義目標類名稱
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"); }
定義一個實例方法的攔截點:
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
實現一個實例方法攔截器,須要實現接口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); }
在before
、after
和exception
環境使用這些核心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),並可能意料以外的結果和反作用。
咱們歡迎你們貢獻插件。
請按照如下步驟操做:
提交一個有關您要貢獻哪些插件的問題,包括支持的版本。
在 apm-sniffer/apm-sdk-plugin
或者apm-sniffer/optional-plugins
模塊下建立自模塊,插件項目的名稱須要包含支持的庫的名稱和版本
按照本指南進行開發。 確保提供了註釋和測試用例。
開發和測試。
提供自動測試用例。如何編寫插件測試用例,能夠參考此文檔。
發送pr並申請review
插件提交者審批經過提交的插件,插件CI-with-IT,e2e和插件測試經過。
SkyWalking接受插件。