此文章翻译了skywalking官方的Java-Plugin-Development-Guide.md
这篇文档主要介绍理解,开发和贡献插件
概念
Span(跨度)
在分布式的链路追踪系统里面Span是一个重要而又普遍的概念。我们可以从Google Dapper Paper 和 OpenTracing学习span的相关知识
Skywalking从2017年就支持OpenTracing和OpenTracing-Java API。我们的Span概念和OpenTracing以及google的论文里面的概念非常相思。而且我们也扩展了Span。
这里有三中类型的Span
1.1 EntrySpan
EntrySpan代表了一个服务提供者,也就是服务端。作为一个APM系统,我们关注应用服务器。所以几乎所有的服务和MQ的消费端都是EntrySpan。
1.2 LocalSpan
LocalSpan可以理解为一个和远程服务无关的普通的Java方法,且这个Java方法即不是MQ的生产者,也不是消费者,更不是一个HTTP服务的生产者和消费者。
1.3 ExitSpan
ExitSpan代表服务的客户端或者MQ的生产者,在Skywalking的早期版本里面名字是LeafSpan。例如通过JDBC访问DB,从Redis或者Memcached读取数据都被归类为ExitSpan。
ContextCarrier(上下文载体)
为了实现分布式的链路追踪,跨进城的追踪需要被绑定,上下文环境需要跨进程传播,这就是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);
ContextSnapshot(上下文快照)
除了跨进程,跨线程也需要得到支持,因为异步执行(内存中的MQ)和批处理在Java中很常见。 跨进程和跨线程是相似的,因为它们都是关于传播上下文。 唯一的区别是,跨线程不需要序列化。
这是跨线程传播的三个步骤:
1.使用ContextManager#capture
获取ContextSnapshot
对象。 2.让子线程通过方法参数或由现有参数携带的任何方式访问ContextSnapshot
。 3.在子线程中使用ContextManager#continued
。
Core APIs(核心API)
ContextManager
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
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 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 文档。
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();
-
在原始上下文中调用
#prepareForAsync
。 -
完成当前线程中的工作后,在原始上下文中执行
ContextManager#stopSpan
。 -
将跨度传播到任何其他线程。
-
完成所有设置后,在任何线程中调用
#asyncFinish
。 -
跟踪上下文将完成,并在所有跨度的
#prepareForAsynsc
完成时向后端报告(由API执行次数判断)。
Develop a plugin(开发插件)
Abstract
追踪的几本方法是使用字节码技术或者AOP来拦截Java方法。
Skywalking封装了字节码操作并追踪上下午传播,所以你只需要定义拦截点(在Spring中也称为切入点)即可。
Intercept
Skywalking提供了两种通用的拦截构造函数的方法:实例方法和类方法。
-
扩展
ClassInstanceMethodsEnhancePluginDefine
类,定义Constructor
拦截点和instance method
拦截点。 -
扩展
ClassStaticMethodsEnhancePluginDefine
定义类方法拦截点。
当然,你也可以扩展ClassEnhancePluginDefine
,来设置所有的拦截点,但是一般不这么用。
Implement plugin
下面演示如何通过扩展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
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); }
在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),并可能意料之外的结果和副作用。
贡献插件到Apache SkyWalking仓库
我们欢迎大家贡献插件。
请按照以下步骤操作:
-
提交一个有关您要贡献哪些插件的问题,包括支持的版本。
-
在
apm-sniffer/apm-sdk-plugin
或者apm-sniffer/optional-plugins
模块下创建自模块,插件项目的名称需要包含支持的库的名称和版本 -
按照本指南进行开发。 确保提供了注释和测试用例。
-
开发和测试。
-
提供自动测试用例。如何编写插件测试用例,可以参考此文档。
-
发送pr并申请review
-
插件提交者审批通过提交的插件,插件CI-with-IT,e2e和插件测试通过。
-
SkyWalking接受插件。
来源:oschina
链接:https://my.oschina.net/mingshashan/blog/3165512