™技术博客

apm | 探针插件开发

2021年6月14日

引言

追踪的基本方法是拦截Java方法,使用字节码操作技术和AOP概念。SkyWalking包装了字节码操作技术,和追踪上下文的传播,所以你只需要定义拦截点(类似于 spring的切面)

拦截

SkyWalking 提供了两类通用的定义来拦截构造方法,实例方法和类方法。

  • ClassInstanceMethodsEnhancePluginDefine 定义了构造方法 constructor 拦截点 和实例方法 instance method 拦截点
  • ClassStaticMethodsEnhancePluginDefine 定义了类方法 class method 拦截点

当然,你也可以继承 ClassEnhancePluginDefine 去设置所有拦截点,但这不常用。

插件实现

通过扩展 ClassInstanceMethodsEnhancePluginDefine 来演示如何实现一个插件。

  1. 定义目标类的名称
    1
    protected abstract ClassMatch enhanceClass();
    ClassMatch 表示如何去匹配目标类,有4种方式:
  • byName: 基于类的全限定名(包名 + . + 类名)。
  • byClassAnnotationMatch: 根据目标类中是否存在某些注解。
  • byMethodAnnotationMatch: 根据目标类的方法中是存在某些注解。
  • byHierarchMatch: 基于目标类的父类或接口。

注意:

  • 在插件定义中禁止使用ThirdPartyClass.class,例如takesArguments(ThirdPartyClass.class) 或byName(ThirdPartyClass.class.getName()),因为 ThirdPartyClass 不一定存在于目标应用程序中,这样做导致探针异常; 我们有“导入”检查来帮助在 CI 流程检查此限制,但它没有涵盖此限制的所有场景,所以永远不要视图通过使用完全限定类名 (FQCN) 之类的方法绕过这个此限制, takeArguments( full.qualified.ThirdPartyClass.class)byName(full.qualified.ThirdPartyClass.class.getName()) 即使能通过CI检查,但是代理代码中仍然无效,使用完全限定的类名字符串文献替代。
  • 禁止使用 *.class.getName() 去获取类名,建议使用文本字符串,这事为了避免classloader的问题
  • by*AnnotationMatch 不支持从父类继承来的注解
  • 除非确实有必要,否则不建议使用 byHierarchyMatch,因为使用它可能会触发拦截许多预期之外的方法,会导致性能问题和不稳定。

实例:

1
2
3
4
@Override
protected ClassMatch enhanceClassName() {
return byName("org.apache.catalina.core.StandardEngineValve");
}
  1. 定义实例方法拦截点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

  1. 在文件 skywalking-plugin.def 中添加插件定义
    1
    tomcat-7.x/8.x=TomcatInstrumentation

    实现一个拦截器

    作为一个实例方法的拦截器,它必须实现
    org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /**
    * A interceptor, which intercept method's invocation. The target methods will be defined in {@link
    * ClassEnhancePluginDefine}'s subclass, most likely in {@link ClassInstanceMethodsEnhancePluginDefine}
    */
    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);
    }
    在方法调用前、调用后以及异常处理阶段使用核心API。

引导类插件

SkyWalking 已经将引导工具打包在代理核心中。 通过在插件定义中声明它,可以很容易继承实现它。
重写 public boolean isBootstrapInstrumentation() 并返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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核心(rt.jar),随便定义它可能会产生意想不到的结果或副作用。

以Tomcat插件为例

定义目标类的名称/定义实例方法拦截点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TomcatInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {

@Override
protected ClassMatch enhanceClass() {
return byName("org.apache.catalina.core.StandardHostValve");
}

@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[] {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named("invoke");
}

@Override
public String getMethodsInterceptor() {
return "org.apache.skywalking.apm.plugin.tomcat78x.TomcatInvokeInterceptor";
}

@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
}

在文件 skywalking-plugin.def 中添加插件定义

1
tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define.TomcatInstrumentation

实现拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {

private static boolean IS_SERVLET_GET_STATUS_METHOD_EXIST;
private static final String SERVLET_RESPONSE_CLASS = "javax.servlet.http.HttpServletResponse";
private static final String GET_STATUS_METHOD = "getStatus";

static {
IS_SERVLET_GET_STATUS_METHOD_EXIST = MethodUtil.isMethodExist(
TomcatInvokeInterceptor.class.getClassLoader(), SERVLET_RESPONSE_CLASS, GET_STATUS_METHOD);
}

/**
* * The {@link TraceSegment#refs} of current trace segment will reference to the trace segment id of the previous
* level if the serialized context is not null.
*
* @param result change this result, if you want to truncate the method.
*/
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
Request request = (Request) allArguments[0];
ContextCarrier contextCarrier = new ContextCarrier();

CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
next.setHeadValue(request.getHeader(next.getHeadKey()));
}

AbstractSpan span = ContextManager.createEntrySpan(request.getRequestURI(), contextCarrier);
Tags.URL.set(span, request.getRequestURL().toString());
Tags.HTTP.METHOD.set(span, request.getMethod());
span.setComponent(ComponentsDefine.TOMCAT);
SpanLayer.asHttp(span);

if (TomcatPluginConfig.Plugin.Tomcat.COLLECT_HTTP_PARAMS) {
collectHttpParam(request, span);
}
}

@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable {
Request request = (Request) allArguments[0];
HttpServletResponse response = (HttpServletResponse) allArguments[1];

AbstractSpan span = ContextManager.activeSpan();
if (IS_SERVLET_GET_STATUS_METHOD_EXIST && response.getStatus() >= 400) {
span.errorOccurred();
Tags.STATUS_CODE.set(span, Integer.toString(response.getStatus()));
}
// Active HTTP parameter collection automatically in the profiling context.
if (!TomcatPluginConfig.Plugin.Tomcat.COLLECT_HTTP_PARAMS && span.isProfiling()) {
collectHttpParam(request, span);
}
ContextManager.getRuntimeContext().remove(Constants.FORWARD_REQUEST_FLAG);
ContextManager.stopSpan();
return ret;
}

@Override
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
Class<?>[] argumentsTypes, Throwable t) {
AbstractSpan span = ContextManager.activeSpan();
span.log(t);
}

private void collectHttpParam(Request request, AbstractSpan span) {
final Map<String, String[]> parameterMap = new HashMap<>();
final org.apache.coyote.Request coyoteRequest = request.getCoyoteRequest();
final Parameters parameters = coyoteRequest.getParameters();
for (final Enumeration<String> names = parameters.getParameterNames(); names.hasMoreElements(); ) {
final String name = names.nextElement();
parameterMap.put(name, parameters.getParameterValues(name));
}

if (!parameterMap.isEmpty()) {
String tagValue = CollectionUtil.toString(parameterMap);
tagValue = TomcatPluginConfig.Plugin.Http.HTTP_PARAMS_LENGTH_THRESHOLD > 0 ?
StringUtil.cut(tagValue, TomcatPluginConfig.Plugin.Http.HTTP_PARAMS_LENGTH_THRESHOLD) :
tagValue;
Tags.HTTP.PARAMS.set(span, tagValue);
}
}
}

引导类插件

Tags: apm

扫描二维码,分享此文章