pinpoint插件开发之二:从零开始新建一个插件

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 从零开始新建pinpoint插件,本篇给出从编码到部署运行的详细步骤

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码): https://github.com/zq2599/blog_demos

本篇概览

开发pinpoint的经验小结

  • 对初学者来说,从最基础的pinpoint体验,到个性化插件开发,应该是个逐步学习的过程,简单来说分为以下步骤:
  1. 对pinpoint整体功能、pinpoint server和pinpoint agent有简单的了解;
  2. 学会编译构建pinpoint;
  3. 对pinpoint 插件的部署有简单的了解;
  4. 实战插件开发;
  • 所以建议您按照以下步骤来逐步实践:
  1. 《Docker下,极速体验编译pinpoint1.6.x分支》
  2. 《Docker下,极速体验pinpoint1.6.3》;
  3. 《pinpoint插件开发之一:牛刀小试,调整gson插件》;
  4. 本章,开发一个全新的插件;
  • 建议您先快速浏览上述三篇文章,然后咱们再一起动手从零开始做一个完成的插件,并在web应用中体验这个插件的功能;

新插件的功能

  • 新做的插件用来做什么呢?
  • 实际业务的生产环境中,常通过日志查看一些程序运行时的信息,例如把用户id打印到日志中,所以我打算做个插件将这些信息在pinpoint上显示出来;

对业务代码的侵入性

  • 为了避免这个功能导致大量修改已有业务代码(侵入性),我的方法是拦截sl4j日志的info这个方法,也就是ch.qos.logback.classic.Logger类的info方法,把Logger.info(String str)的str参数在pinpoint输出;

特殊的约定

  • 生产环境中到处都调用了sl4j的info方法,如果全部拦截内容就太多了,所以和业务做个约定,info方法入参的字符串,如果以pinpoint_bizlog_为前缀,那么我们的插件才做拦截,将日志的信息打印出来,其他的保持原样;
  • 这样只要业务执行诸如logger.info("pinpoint_bizlog_" + "userid :" + userid);这样的代码,我们的插件就会将userid : xxx这样的字符串在pinpoint的追踪信息中显示出来;

开始吧

  • 功能已经设计好了,那我们就开始吧,给插件起个名字:bizlog

下载pinpoint源码

这里写图片描述

导入到ide

  • 整个pinpoint是一个大的maven工程,里面有很多小工程,所以我们用idea来导入;

创建新的maven工程

  • 在plugins文件夹下新建一个bizlog目录,里面新增一个pom.xml文件,内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.navercorp.pinpoint</groupId>
        <artifactId>pinpoint</artifactId>
        <relativePath>../..</relativePath>
        <version>1.6.3-SNAPSHOT</version>
    </parent>

    <artifactId>pinpoint-bizlog-plugin</artifactId>
    <name>pinpoint-bizlog-plugin</name>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.navercorp.pinpoint</groupId>
            <artifactId>pinpoint-bootstrap-core</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
  • 将自己作为pinpoint的子工程,依赖的是pinpoint-bootstrap-core这个库;

通过配置指定插件类和元信息类

  • 需要通过配置文件告诉pinpoint当前插件的功能类和元数据类在哪里,配置文件有两个,都放在src/main/java/resources/META-INF/services目录,如下图:

这里写图片描述

  • 文件名:com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
  • 文件内容:com.navercorp.pinpoint.plugin.bizlog.BizlogPlugin
  • 文件功能:指定插件功能类
  • 文件名:com.navercorp.pinpoint.common.trace.TraceMetadataProvider
  • 文件内容:com.navercorp.pinpoint.plugin.bizlog.BizlogMetadataProvider
  • 文件功能:指定元数据类

开发插件功能类

  • 在src\main\java\com\navercorp\pinpoint\plugin\bizlog目录下创建功能类BizPlugin.java:
package com.navercorp.pinpoint.plugin.bizlog;

import com.navercorp.pinpoint.bootstrap.instrument.InstrumentClass;
import com.navercorp.pinpoint.bootstrap.instrument.InstrumentException;
import com.navercorp.pinpoint.bootstrap.instrument.InstrumentMethod;
import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor;
import com.navercorp.pinpoint.bootstrap.instrument.MethodFilters;
import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback;
import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformTemplate;
import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformTemplateAware;
import com.navercorp.pinpoint.bootstrap.logging.PLogger;
import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin;
import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPluginSetupContext;
import com.navercorp.pinpoint.common.trace.AnnotationKey;
import com.navercorp.pinpoint.common.trace.AnnotationKeyFactory;
import com.navercorp.pinpoint.common.trace.ServiceType;
import com.navercorp.pinpoint.common.trace.ServiceTypeFactory;

import java.security.ProtectionDomain;

/**
 * @author willzhao
 */
public class BizlogPlugin implements ProfilerPlugin, TransformTemplateAware {
    //BIZLOG_SERVICE_TYPE是bizlog插件的身份定义,用了1998这个id
    public static final ServiceType BIZLOG_SERVICE_TYPE = ServiceTypeFactory.of(1998, "BIZLOG");

    //BIZLOG_ANNOTATION_KEY_INFO是打算在pinpoint追踪信息中显示的属性的定义,用了9998这个id
    public static final AnnotationKey BIZLOG_ANNOTATION_KEY_INFO = AnnotationKeyFactory.of(9998, "bizlog.info", com.navercorp.pinpoint.common.trace.AnnotationKeyProperty.VIEW_IN_RECORD_SET);

    private static final String BIZLOG_SCOPE = "BIZLOG_SCOPE";

    private final PLogger logger = PLoggerFactory.getLogger(this.getClass());

    private TransformTemplate transformTemplate;

    @Override
    public void setup(ProfilerPluginSetupContext context) {
            //Logger类被加载的时候,会注入这里new的TransformCallback,对这个类的实例在线程中的行为进行拦截
            transformTemplate.transform("ch.qos.logback.classic.Logger", new TransformCallback() {

                @Override
                public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
                    InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
                    
                    //找到所有名为info的方法
                    for (InstrumentMethod m : target.getDeclaredMethods(MethodFilters.name("info"))) {
                        ////注入Interceptor,在Logger类的实例执行info方法的时候会执行这个interceptor
                        m.addScopedInterceptor("com.navercorp.pinpoint.plugin.bizlog.interceptor.BizlogInterceptor", BIZLOG_SCOPE);
                    }

                    return target.toBytecode();
                }
            });
    }


    @Override
    public void setTransformTemplate(TransformTemplate transformTemplate) {
        this.transformTemplate = transformTemplate;
    }
}
  • 以上方法对ch.qos.logback.classic.Logger类就行了注入,在Logger类的实例的info方法被调用时注入的Interceptor就会被执行;

开发元信息类

  • 在src\main\java\com\navercorp\pinpoint\plugin\bizlog目录下创建元信息类BizlogMetadataProvider.java:
package com.navercorp.pinpoint.plugin.bizlog;

import com.navercorp.pinpoint.common.trace.TraceMetadataProvider;
import com.navercorp.pinpoint.common.trace.TraceMetadataSetupContext;

/**
 * @author willzhao
 */
public class BizlogMetadataProvider implements TraceMetadataProvider {
    /**
     * @see TraceMetadataProvider#setup(TraceMetadataSetupContext)
     */
    @Override
    public void setup(TraceMetadataSetupContext context) {
        //设定当前插件的ServiceType,既插件的唯一身份
        context.addServiceType(BizlogPlugin.BIZLOG_SERVICE_TYPE);
        //设定当前插件要展示的参数
        context.addAnnotationKey(BizlogPlugin.BIZLOG_ANNOTATION_KEY_INFO);
    }
}
  • 以上方法会被pinpoint调用,这样pinpoint就知道了我们这次新增的这个插件了,以及我们要在pinpoint中显示的参数;

拦截器BizlogInterceptor

  • 拦截器是Logger类被加载的时候被pinpoint注入的,被拦截的方法在执行前后所做的事情都在拦截器中定义,以下就是BizlogInterceptor:
package com.navercorp.pinpoint.plugin.bizlog.interceptor;

import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor;
import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder;
import com.navercorp.pinpoint.bootstrap.context.Trace;
import com.navercorp.pinpoint.bootstrap.context.TraceContext;
import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
import com.navercorp.pinpoint.bootstrap.logging.PLogger;
import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
import com.navercorp.pinpoint.plugin.bizlog.BizlogPlugin;

/**
 * logger info method interceptor
 *
 * @author willzhao
 */
public class BizlogInterceptor implements AroundInterceptor {
    private final TraceContext traceContext;
    private final MethodDescriptor descriptor;
    private final PLogger logger = PLoggerFactory.getLogger(getClass());

    public BizlogInterceptor(TraceContext traceContext, MethodDescriptor descriptor) {
        this.traceContext = traceContext;
        this.descriptor = descriptor;
    }

    private static boolean shouldTrace(Object[] args){
        return null!=args
                && args.length>0
                && (args[0] instanceof String)
                && ((String)args[0]).indexOf("pinpoint_bizlog_name")>-1;
    }

    @Override
    public void before(Object target, Object[] args) {
        if (logger.isDebugEnabled()) {
            logger.beforeInterceptor(target, args);
        }

        final Trace trace = traceContext.currentTraceObject();
        if (trace == null) {
            return;
        }

        if(!shouldTrace(args)){
            return;
        }

        trace.traceBlockBegin();

    }

    @Override
    public void after(Object target, Object[] args, Object result, Throwable throwable) {
        if (logger.isDebugEnabled()) {
            logger.afterInterceptor(target, args);
        }

        Trace trace = traceContext.currentTraceObject();
        if (trace == null) {
            return;
        }

        if(!shouldTrace(args)){
            return;
        }

        try {
            SpanEventRecorder recorder = trace.currentSpanEventRecorder();
               recorder.recordServiceType(BizlogPlugin.BIZLOG_SERVICE_TYPE);
            recorder.recordApi(descriptor);
            recorder.recordException(throwable);
            recorder.recordAttribute(BizlogPlugin.BIZLOG_ANNOTATION_KEY_INFO, args[0]);
        } finally {
            trace.traceBlockEnd();
        }
    }
}
  • 上述的代码中,before和after方法分别代表logger.info方法执行前和执行后拦截器所做的事情,shouldTrace方法检查入参中是否有"pinpoint_bizlog_name"前缀,如果没有就不执行拦截操作了,如果有,就执行trace操作,recorder.recordAttribute会将入参记录并在pinpoint追踪信息中展示出来;

plugins工程的配置

  • 由于我们新建的bizlog工程和其他插件工程一样是plugins的子工程,为了能构建和打包,要在plugins工程中配置,打开plugins文件夹下的pom.xml文件:
  • 首先,在modeles节点中增加以下内容:
<module>bizlog</module>

然后,在dependencies节点增加以下内容:

<dependency>
    <groupId>com.navercorp.pinpoint</groupId>
    <artifactId>pinpoint-bizlog-plugin</artifactId>
    <version>${project.version}</version>
</dependency>

开发小结

  • 除了修改plugins目录下的pom.xml文件,本次开发的插件一共需要新增六个文件:
  1. 插件的pom.xml;
  2. com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin:定义插件功能类
  3. com.navercorp.pinpoint.common.trace.TraceMetadataProvider:定义插件元信息类
  4. BizlogPlugin.java:插件功能类,确定对哪个类的哪些方法做拦截
  5. BizlogMetadataProvider.java:元信息类:确定插件id和插件参数id
  6. BizlogInterceptor.java:拦截类:定义在拦截的时候做什么事情

bizlog插件源码下载

  • bizlog的源码可以在我的git下载,地址是:git@github.com:zq2599/pinpoint163-plugin-develop.git
  • 这里面包含了完整的pinpoint1.6.X分支的源码,bizlog的在plugins目录下,如下图红框所示:

这里写图片描述

编译构建bizlog插件的环境

  • 开发已经完成,接下来就是编译构建bizlog插件了,推荐在Docker环境去构建,在Docker构建pinpoint插件的方法请参照《Docker下,极速体验编译pinpoint1.6.x分支》,和文中略有区别的是,为了方便复制文件我们用以下命令来启动容器(多了个-v参数):
docker run --name=ppcompile001 -p 19003:22 -idt -v c:/share:/usr/Downloads bolingcavalry/jdk7-mvn339-pinpoint16x-compile:0.0.1
  • 这样启动后,当前电脑的c:/share目录和容器的/usr/Downloads目录实际上是同一个位置了;

开始编译构建

  • 编译pinpoint插件的Docker环境准备好后,我们把bizlog插件相关的内容都移植过来吧:
  1. 像上面那样修改ppcompile001容器中plugins目录下的pom.xml文件,给节点和节点增加内容;
  2. 将前面做好的bizlog目录整体复制到ppcompile001容器中的plugins目录下;
  3. 在pinpoint目录下执行编译命令:mvn install -Dmaven.test.skip=true -e
  4. 编译成功后,在bizlog/target目录下可以看到最新的插件,如下图红色字体所示:

这里写图片描述

准备pinpoint环境

  • 为了验证bizlog插件,我们要有包含以下功能的环境:
  1. 有pinpoint server;
  2. 有pinpoint agent;
  3. pinpoint agent上部署了web应用,能被pinpoint追踪;
  • 如何快速准备好这样一套环境呢?请参照《Docker下,极速体验pinpoint1.6.3》一文,能够以最快速度将pinpoint server和pinpoint agent搭建好,然后把web应用部署到pinpoint agent上;

部署bizlog插件

  1. 进入pinpoint server容器,在pinpoint-collector和pinpoint-web两个tomcat server的apache-tomcat-8.0.36/webapps/ROOT/WEB-INF/lib/目录下,放置bizlog的jar包;
  2. 重启collector和web两个tomcat;
  3. 进入tomcat001容器,在pinpoint-agent-1.6.3/plugin/目录下放置bizlog的jar包;
  4. 重启tomcat001容器;

验证bizlog插件

  • 部署在tomcat001上的web应用中,有下面这段代码:
public String tracegson(HttpServletRequest request, Model model) {
        String name = get(request, "name");
        String age = get(request, "age");

        Student student = new Student();
        student.setName(name);
        student.setAge(Integer.valueOf(age));

        Gson gson = new Gson();

        String parseStr = gson.toJson(student, Student.class);

        logger.info("gson str [{}]", parseStr);

        return String.format("gson str : %s [%s]", parseStr, tag());
    }
  • 为了验证bizlog插件,在方法return之前加了下面这两句,两次执行logger.info方法,第一次带上了pinpoint_bizlog_name前缀,第二次没有带:
logger.info("pinpoint_bizlog_name 1. [" + name + "], age [" + age + "]");

logger.info("2. [" + name + "], age [" + age + "]");
  • 加上之后,将web应用部署到tomcat001容器上,访问地址:http://localhost:8081/pinpointtracedemo/tracegson?name=tom&age=11
  • 然后去pinpoint上看一下,如下图:

这里写图片描述

  • 可以看到我们的插件已经出现在红框位置,而且只将“pinpoint_bizlog_name”前缀的log信息打印出来,今后需要通过pinpoint追踪的信息,都可以通过logger.info("pinpoint_bizlog_xxxxxx的方式来实现;
  • 以上就是开发一个完整插件的过程,希望能对您有所帮助,也祝您顺利开发出自己需要的插件;

了解pinpoint编译环境的更多细节

了解pinpoint server、pinpoint agent部署的更多细节

欢迎关注阿里云开发者社区博客:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...
相关实践学习
日志服务之数据清洗与入湖
本教程介绍如何使用日志服务接入NGINX模拟数据,通过数据加工对数据进行清洗并归档至OSS中进行存储。
相关文章
|
2月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的计算机office课程平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的计算机office课程平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
2月前
|
缓存 移动开发 监控
Star 1.3K!推荐一款可以远程调试任意Web项目的开源工具!
Star 1.3K!推荐一款可以远程调试任意Web项目的开源工具!
|
资源调度
插件开发步骤
插件开发步骤
122 0
|
Java Maven 开发者
搭建项目环境(创建工程) | 学习笔记
快速学习搭建项目环境(创建工程)
86 0
|
JSON JavaScript Go
一日一技:如何正确在自己项目里面集成别人的代码?
一日一技:如何正确在自己项目里面集成别人的代码?
488 0
一日一技:如何正确在自己项目里面集成别人的代码?
|
机器学习/深度学习 NoSQL Java
《IntelliJ IDEA 插件开发》第四节:扩展创建工程向导步骤,开发DDD脚手架
一、前言 二、需求目的 三、案例开发 1. 工程结构 2. UI 工程配置窗体 3. 配置工程步骤创建 4. 开发脚手架服务 5. 调用脚手架服务 6. 配置模板工程 四、测试验证 五、总结 六、系列推荐
798 0
《IntelliJ IDEA 插件开发》第四节:扩展创建工程向导步骤,开发DDD脚手架
|
Java 测试技术 API
《IntelliJ IDEA 插件开发》第一节:两种方式创建插件工程
一、前言 二、需求目的 三、环境说明 四、模板方式创建 1. 创建引导 2. 工程结构 3. plugin.xml 配置 4. MyAction 事件入口 5. 运行测试 五、Gradle 方式创建 1. 创建引导 2. 工程结构 3. build.gradle 配置 4. MyAction 事件入口 5. 运行测试 六、总结
1232 0
《IntelliJ IDEA 插件开发》第一节:两种方式创建插件工程
|
Java Maven Spring
springboot高级功能(十一)聚合工程讲解与部署
springboot高级功能(十一)聚合工程讲解与部署
205 0
springboot高级功能(十一)聚合工程讲解与部署
|
关系型数据库 数据库 PostgreSQL
如何使用代码生成插件easycode
如何使用代码生成插件easycode
162 0
如何使用代码生成插件easycode

热门文章

最新文章

  • 1
    流量控制系统,用正则表达式提取汉字
    25
  • 2
    Redis09-----List类型,有序,元素可以重复,插入和删除快,查询速度一般,一般保存一些有顺序的数据,如朋友圈点赞列表,评论列表等,LPUSH user 1 2 3可以一个一个推
    26
  • 3
    Redis08命令-Hash类型,也叫散列,其中value是一个无序字典,类似于java的HashMap结构,Hash结构可以将对象中的每个字段独立存储,可以针对每字段做CRUD
    26
  • 4
    Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
    27
  • 5
    S外部函数可以访问函数内部的变量的闭包-闭包最简单的用不了,闭包是内层函数+外层函数的变量,简称为函数套函数,外部函数可以访问函数内部的变量,存在函数套函数
    24
  • 6
    Redis06-Redis常用的命令,模糊的搜索查询往往会对服务器产生很大的压力,MSET k1 v1 k2 v2 k3 v3 添加,DEL是删除的意思,EXISTS age 可以用来查询是否有存在1
    30
  • 7
    Redis05数据结构介绍,数据结构介绍,官方网站中看到
    22
  • 8
    JS字符串数据类型转换,字符串如何转成变量,+号只要有一个是字符串,就会把另外一个转成字符串,- * / 都会把数据转成数字类型,数字型控制台是蓝色,字符型控制台是黑色,
    20
  • 9
    JS数组操作---删除,arr.pop()方法从数组中删除最后一个元素,并返回该元素的值,arr.shift() 删除第一个值,arr.splice()方法,删除指定元素,arr.splice,从第一
    20
  • 10
    定义好变量,${age}模版字符串,对象可以放null,检验数据类型console.log(typeof str)
    19