“ Pinpoint是款非常优秀的APM工具,但是其默认实现的功能并不能满足我们的需求,今天我们就来说说如何定制自己的插件。”
01
—
背景
前面文章我们曾介绍过Pinpoint这款非常优秀的APM工具(参见《如何将业务应用日志与Pinpoint链路关联起来》),但是其默认实现的功能并不能满足我们的需求,如2.0之前版本不支持多线程链路展示。这时就需要我们自行定制插件了,今天我们就来说说如何定制自己的插件。02
—
插件装配原理
首先来看一张图,pinpoint plugin的目录结构: 是不是很熟悉,没错,Pinpoint的插件机制就是基于java SPI的ServiceLoader机制实现的。 Pinpoint agent在启动的时候会加载plugins文件夹下所有的插件。它会扫描插件jar包中 META-INF/services 目录下的两个配置文件来确认ProfilerPlugin和TraceMetadataProvider的实现类。 META-INF/services/com.naercorp.pinpoint.bootstrap.plugin.ProfilerPlugin:com.navercorp.pinpoint.plugin.thread.ThreadPlugin
META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider:
com.navercorp.pinpoint.plugin.thread.ThreadTypeProvider
03
—
元数据注册
package com.navercorp.pinpoint.common.trace;public interface TraceMetadataProvider { void setup(TraceMetadataSetupContext var1);}
里面只有一个setup方法,我们实现类使用setup方法注册定制的ServiceType和AnnotationKey:
public class ThreadTypeProvider implements TraceMetadataProvider { @Override public void setup(TraceMetadataSetupContext context) { context.addServiceType(ThreadConstants.SERVICE_TYPE); context.addAnnotationKey(ThreadConstants.ZEYE_TRACEID_ANNOTATION_KEY); }}...此处省略1w行代码...public static final ServiceType SERVICE_TYPE = ServiceTypeFactory.of(6001, SCOPE_NAME);public static final AnnotationKey ZEYE_TRACEID_ANNOTATION_KEY = AnnotationKeyFactory.of(919, "zeye.traceId", new AnnotationKeyProperty[]{AnnotationKeyProperty.VIEW_IN_RECORD_SET});
需要注意的是这里,ServiceType和AnnotationKey的code都是有范围的,这部分内容官方文档很详细了。
04
—
开始开发自己的插件自定义插件必须实现ProfilerPlugin接口,且只需要实现一个setup方法。在这个方法里,我们需要做两件事:- 配置检测
- 给指定类添加注册TransformCallback类
@Override public void setup(ProfilerPluginSetupContext context) { ThreadConfig threadConfig = new ThreadConfig(context.getConfig()); // #1 String threadMatchPackages = threadConfig.getThreadMatchPackage(); if (StringUtils.isEmpty(threadMatchPackages)) { logger.info("thread plugin package is empty,skip it"); return; } List<String> threadMatchPackageList = StringUtils.tokenizeToStringList(threadMatchPackages, ","); for (String threadMatchPackage : threadMatchPackageList) { addRunnableInterceptor(threadMatchPackage); // #2 addCallableInterceptor(threadMatchPackage); // #3 } }
小编这里举例的是多线程插件,上面片段代码中#1为读取相关配置项,配置文件是pinpoint.config。这里的配置项是根据package前缀匹配类名。
############################################################ Thread############################################################ which package of runnable(callable) instance can be thread plugin trace# Set the package name to track# eg) profiler.thread.match.package=com.company.shopping.cartprofiler.thread.match.package=cn.gov.zcy.thread,cn.gov.zcy.dubbo.test
#2和#3是为匹配到的类注册拦截器。
private void addRunnableInterceptor(String threadMatchPackage) { Matcher matcher = Matchers.newPackageBasedMatcher(threadMatchPackage, new InterfaceInternalNameMatcherOperand("java.lang.Runnable", true)); transformTemplate.transform(matcher, new RunnableTransformCallback()); }private void addCallableInterceptor(String threadMatchPackage) { Matcher matcher = Matchers.newPackageBasedMatcher(threadMatchPackage, new InterfaceInternalNameMatcherOperand("java.util.concurrent.Callable", true)); transformTemplate.transform(matcher, new CallableTransformCallback()); }
拦截器工作原理示意图(图片来自网络)
上面两个方法注册拦截器逻辑相似,我们以transformTemplate.transform(matcher, new RunnableTransformCallback())为例进行讲解。
public static class RunnableTransformCallback implements TransformCallback { @Override public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException { final InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer); logger.info("ThreadPlugin targetName: {}", target.getName()); List allConstructor = new ArrayList(); allConstructor.add(target.getConstructor(null)); // #4 注册构造方法拦截器 for (InstrumentMethod instrumentMethod : allConstructor) { logger.info("ThreadPlugin className: {} instrumentMethod: {}", className, instrumentMethod); instrumentMethod.addScopedInterceptor(ThreadConstructorInterceptor.class.getName(), ThreadConstants.SCOPE_NAME, ExecutionPolicy.ALWAYS); } // #5 动态注册属性,主要用来做数据传递 target.addField(AsyncContextAccessor.class.getName()); target.addField(ZeyeTraceIdAccessor.class.getName()); // #6 注册方法拦截器 final InstrumentMethod runMethod = target.getDeclaredMethod("run"); if (runMethod != null) { logger.info("runMethod: {}", runMethod.getName()); runMethod.addInterceptor(ThreadCallInterceptor.class.getName()); } // #7 返回类字节码 return target.toBytecode(); } }
关键步骤的注释已经进行了说明。这里要说明的一点是,本例中同时注册了构造方法拦截器和方法拦截器,原因是本例是做多线程上下文传递的,即需要在创建多线程时将主线程的上下文信息传递给子线程。
继续看ThreadConstructorInterceptor拦截器的具体实现:
// #8 自定义拦截器必须实现AroundInterceptor或其子类,AroundInterceptor根据参数的个数有多个实现public class ThreadConstructorInterceptor implements AroundInterceptor { private final PLogger logger = PLoggerFactory.getLogger(this.getClass()); private final boolean isDebug = logger.isDebugEnabled(); private TraceContext traceContext; private MethodDescriptor descriptor; public ThreadConstructorInterceptor(TraceContext traceContext, MethodDescriptor descriptor) { this.traceContext = traceContext; this.descriptor = descriptor; } @Override public void before(Object target, Object[] args) { logger.info("ThreadConstructorInterceptor before"); if (isDebug) { logger.beforeInterceptor(target, args); } // #9 获取当前链路追踪上下文 final Trace trace = traceContext.currentTraceObject(); if (trace == null) { return; } // #10 开启一个新的span final SpanEventRecorder recorder = trace.traceBlockBegin(); // #11 匹配前文中的addField,这里设置field的值 if (target instanceof AsyncContextAccessor) { final AsyncContext asyncContext = recorder.recordNextAsyncContext(); ((AsyncContextAccessor) target)._$PINPOINT$_setAsyncContext(asyncContext); } // #12 匹配前文中的addField,这里设置field的值 if (target instanceof ZeyeTraceIdAccessor) { // 兼容自研zeye String traceId = MDC.get("traceId"); ((ZeyeTraceIdAccessor) target)._$PINPOINT$_setZeyeTraceId(traceId); } } @Override public void after(Object target, Object[] args, Object result, Throwable throwable) { if (isDebug) { logger.afterInterceptor(target, args, result, throwable); } final Trace trace = traceContext.currentTraceObject(); logger.info("ThreadConstructorInterceptor after trace: {}", trace); if (trace == null) { return; } try { // #13 记录span信息 final SpanEventRecorder recorder = trace.currentSpanEventRecorder(); recorder.recordApi(this.descriptor); recorder.recordServiceType(ThreadConstants.SERVICE_TYPE); recorder.recordException(throwable); // #14 上面几项是pinpoint默认属性,这里的recordAttribute可以定制自己的属性,并展示在链路查看页面上 recorder.recordAttribute(ThreadConstants.ZEYE_TRACEID_ANNOTATION_KEY, MDC.get("traceId")); } finally { trace.traceBlockEnd(); } } }
Pinpoint的AroundInterceptor有点类似spring AOP中的around切面,但相对简单很多,我们只需要按照约定实现自己的逻辑即可。
ThreadCallInterceptor拦截器实现和上面的ThreadConstructorInterceptor类似,这里就不做多余说明,直接贴出代码,小伙伴可以自己阅读。
public class ThreadCallInterceptor extends AsyncContextSpanEventSimpleAroundInterceptor { private final PLogger logger = PLoggerFactory.getLogger(this.getClass()); public ThreadCallInterceptor(TraceContext traceContext, MethodDescriptor methodDescriptor) { super(traceContext, methodDescriptor); } @Override protected void doInBeforeTrace(SpanEventRecorder recorder, AsyncContext asyncContext, Object target, Object[] args) { try { logger.info("ThreadCallInterceptor doInBeforeTrace: {}, {}", args, target); if (target instanceof ZeyeTraceIdAccessor) { String traceId = ((ZeyeTraceIdAccessor) target)._$PINPOINT$_getZeyeTraceId(); logger.info("ThreadCallInterceptor doInBeforeTrace Zeye traceId: {}", traceId); if (traceId != null && !"".equals(traceId)) { MDC.put("traceId", traceId); } } } catch (Exception e) { e.printStackTrace(); } } @Override protected void doInAfterTrace(SpanEventRecorder recorder, Object target, Object[] args, Object result, Throwable throwable) { recorder.recordApi(methodDescriptor); recorder.recordServiceType(ThreadConstants.SERVICE_TYPE); recorder.recordException(throwable); recorder.recordAttribute(ThreadConstants.ZEYE_TRACEID_ANNOTATION_KEY, MDC.get("traceId")); }}
这里需要注意的是多线程拦截器必须实现AsyncContextSpanEventSimpleAroundInterceptor接口。
05
—
发布插件最后,我们将自己编写的插件打包,生成的jar包丢到pinpoint-agent/plugins/目录下就可以了。最终本文定制的多线程插件实现效果如下图示:
参考文档:
https://github.com/naver/pinpoint/tree/master/plugins
https://naver.github.io/pinpoint/plugindevguide.html