此文章翻译了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接受插件。