PDF书籍《手写调用链监控APM系统-Java版》第7章 插件与链路的结合:Tomcat插件实现

本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 “调用链监控APM” 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。

作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。

本书涉及到的核心技术与思想

JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。

适用人群

自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;

版权

本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。

原版PDF+源码请见:

本章涉及到的工具类也在这里面:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

第7章 插件与链路的结合:Tomcat插件实现

通过前面的章节,我们已经把所有基建工程开发完成了,本章就是制作各种插桩插件,通过这些插件的修改原始调用字节码来实现创建链路以及上报链路数据。

7.1 Tomcat插件实现与测试

制作这个插件是为了上报http的get,post请求链路数据,链路数据包含请求的url,请求时间,服务名等,也就是链路的span的一些信息。

拦截这个插件就需要你要了解当一个请求发生时,会必须执行tomcat的哪个类的哪个方法,这里我直接告诉你答案:

类名:org.apache.catalina.core.StandardHostValve

方法:invoke

非JDK类库

首先先将tomcat库的依赖加上,在tomcat-plugin的pom中添加:

<dependency><groupId>com.hadluo.apm</groupId><artifactId>apm-commons</artifactId><version>1.0</version><scope>compile</scope>
</dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId><version>8.5.43</version><scope>provided</scope>
</dependency>
<dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><version>8.5.43</version><scope>provided</scope>
</dependency>

根据上面描述的拦截信息,修改我们之前的tomcat-plugin的测试代码:

com.hadluo.apm.plugin.tomcat.TomcatInstrumentation

public class TomcatInstrumentation extends AbstractClassEnhancePluginDefine {@Overridepublic String enhanceClass() {// 要增强的 类return "org.apache.catalina.core.StandardHostValve";}@Overridepublic MethodsInterceptPoint[] configMethodsInterceptPoint() {return new MethodsInterceptPoint[]{new MethodsInterceptPoint() {@Overridepublic ElementMatcher<MethodDescription> getMethodsMatcher() {// 拦截 invoke 方法return ElementMatchers.named("invoke");}@Overridepublic String getMethodsInterceptor() {// 拦截逻辑交给 TomcatInvokeInterceptorreturn "com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor";}@Overridepublic boolean isOverrideArgs() {return false;}}};}
}

上述插件定义帮我们声明了要增强的是StandardHostValve类中的invoke方法,然后将增强处理逻辑交给了TomcatInvokeInterceptor拦截器类。如果你忘记了插件的实现,可以回到第4章看看如何实现插件的。

接下来修改TomcatInvokeInterceptor拦截器代码:

com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor

public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {@Overridepublic void beforeMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable {}@Overridepublic Object afterMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {// 需要结束这个spanTraceContextManager service = ServiceManager.INSTANCE.getService(TraceContextManager.class);service.stopSpan();return ret;}@Overridepublic void handleMethodException(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {// 出异常了,需要记录异常// 取出栈顶的 spanTraceContextManager service = ServiceManager.INSTANCE.getService(TraceContextManager.class);AbstractSpan span = service.activeSpan();span.log(t) ;}
}

方法执行后stopSpan ,出异常了记录log,这两个都很简单,beforeMethod稍微麻烦,单独拿出来。

beforeMethod为StandardHostValve的invoke方法执行之前的调用,这里表示是一个请求进来了,所以我们需要创建的是一个EntrySpan,同时,我们还需要解析http中的header数据,防止不是链路的第一层请求。

beforeMethod方法代码实现:

// 获取invoke方法的第一个参数,也就是tomcat请求的Request
Request request = (Request) allArguments[0];
// 将 header的 值 放到 ContextCarrier
ContextCarrier carrier = new ContextCarrier();
Map<String,String> headers = new HashMap<String,String>();
carrier.keys().forEach(key->{if(request.getHeader(key) != null){headers.put(key,request.getHeader(key));}
});
carrier.deserialize(headers);
// 构造 entry span
TraceContextManager service = ServiceManager.INSTANCE.getService(TraceContextManager.class);
AbstractSpan entrySpan = service.createEntrySpan(request.getRequestURI(), carrier);// 设置参数信息
entrySpan.setTag("url", request.getRequestURI());
entrySpan.setTag("http.method", request.getMethod());
entrySpan.setComponent("Tomcat") ;
entrySpan.setLayer(SpanLayer.HTTP);

跨进程传播ContextCarrier 时,我们将ContextCarrier 里面的字段名称作为http header的key,值就作为http header的key值进行传递。

carrier.keys就是获取里面的字段名称集合。在com.hadluo.apm.commons.trace.ContextCarrier类中新增方法:

public Set<String> keys() {Set<String> keys = new HashSet<String>();for (Field f : this.getClass().getDeclaredFields()) {keys.add(SW_FLAG + f.getName());}return keys;
}

我们对http header的key加了一个前缀,防止ContextCarrier字段名和微服务应用要用的http header的key重名。

在com.hadluo.apm.commons.trace.ContextCarrier类中新增字段:

//key标识
private static final String SW_FLAG = "SW_APM_"; 

carrier.deserialize 是一个相当于反序列化的方法,刚构造出来的carrier里面的字段是为空的,需要deserialize 方法就是将http的header中的数据拷贝到carrier字段中的。

在com.hadluo.apm.commons.trace.ContextCarrier类中新增方法:

public void deserialize(Map<String, String> param) {// 获取所有字段for (Field f : this.getClass().getDeclaredFields()) {if (!param.containsKey(SW_FLAG + f.getName())) {continue;}// 含有携带的数据String value = param.get(SW_FLAG + f.getName());f.setAccessible(true);try {f.set(this, value);} catch (IllegalAccessException e) {Logs.err(getClass(), "ContextCarrier deserialize错误, field: " + f.getName() + " ,value:" + value, e);}}
}

beforeMethod后面的逻辑就是创建EntrySpan,然后设置标签等信息,此时就会创建一个链路上下文,包含一个TraceSegment,然后开辟一个存储span的栈空间,入栈第一个EntrySpan,这些都是在第5章做过了详细的介绍了。

至于将ContextCarrier的值设置到http header中的逻辑就不是这个插件完成的,而是发起http调用的插件,比如http client, 是创建ExitSpan时要进行的操作。

到此Tomcat插件代码编写完成。还要注意两点:

1. 在hadluo-apm-plugin.def插件定义文件中声明

2. 在agent-core项目的pom中引用到插件的maven坐标。

这两点之前讲插件都已经完成。此时我们就可以监控tomcat的请求了。接下来我们进行测试。

修改我们测试的微服务controller接口:

@GetMapping("/order")
public String order(@RequestParam("shopId")String shopId) throws ClassNotFoundException {System.out.println("下单请求 商品ID:" + shopId);return UUID.randomUUID().toString();
}

打包apm-agent-core, 启动测试,访问接口。后台会打印出kafka发送,发现我们数据已经上报到kafka了。

将数据格式化:

{"msgTypeClass": "com.hadluo.apm.commons.kafka.Segment","sampleTime": 1733280074533,"serviceName": "smartapm-test","serviceInstance": "4a4bbeaf82f045b6b2055046d2e96860@192.168.2.233","traceId": "f9e28159290b443fbe8323fa5919b1d1.45.17332800535210001","traceSegmentId": "f9e28159290b443fbe8323fa5919b1d1.45.17332800535210000","spans": [{"spanId": 0,"parentSpanId": -1,"startTime": 1733280053525,"endTime": 1733280074531,"refs": [],"operationName": "/order","peer": null,"spanType": "Entry","spanLayer": "HTTP","component": "Tomcat","tags": {"http.method": "GET","url": "/order"},"logs": {}}]
}

这就是一条简单的链路信息,我们可以看出来里面就一个span操作,组件是tomcat,类型是EntrySpan,访问的接口为/order 。

这个json数据其实就是对应一个结束的TraceSegment的信息,包含了里面所有的span,字段意思在第5章讲解链路就已经详细阐述过。

接下来我们还需要测试http接口异常的情况,改写测试接口,编写会抛出零除异常,代码如下:

 @GetMapping("/order")
public String order(@RequestParam("shopId")String shopId) throws ClassNotFoundException {System.out.println("下单请求 商品ID:" + shopId);int i=0;i = 12/i ;return UUID.randomUUID().toString();
}

测试发现上报的链路数据中并没有log出现。原因是handleMethodException并没有执行,也就是说这种异常不会在StandardHostValve类的invoke方法中被抛出来。其实StandardHostValve类还有一个throwable方法,异常会走这个方法。

在插件定义TomcatInstrumentation中多声明一个执行方法,改写

configMethodsInterceptPoint方法:

@Override
public MethodsInterceptPoint[] configMethodsInterceptPoint() {return new MethodsInterceptPoint[]{new MethodsInterceptPoint() {@Overridepublic ElementMatcher<MethodDescription> getMethodsMatcher() {// 拦截 invoke 方法return ElementMatchers.named("invoke");}@Overridepublic String getMethodsInterceptor() {// 拦截逻辑交给 TomcatInvokeInterceptorreturn "com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor";}@Overridepublic boolean isOverrideArgs() {return false;}},new MethodsInterceptPoint() {@Overridepublic ElementMatcher<MethodDescription> getMethodsMatcher() {// 拦截 throwable方法return ElementMatchers.named("throwable");}@Overridepublic String getMethodsInterceptor() {return "com.hadluo.apm.plugin.tomcat.TomcatExceptionInterceptor";}@Overridepublic boolean isOverrideArgs() {return false;}}};
}

增加执行逻辑拦截器TomcatExceptionInterceptor:

public class TomcatExceptionInterceptor implements InstanceMethodsAroundInterceptor {@Overridepublic Object afterMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {return ret;}@Overridepublic void handleMethodException(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {}@Overridepublic void beforeMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable {TraceContextManager service = ServiceManager.INSTANCE.getService(TraceContextManager.class);AbstractSpan span = service.activeSpan();// 第二个参数为 异常span.log((Throwable) allArguments[2]) ;}
}

在 beforeMethod中,也就是throwable方法执行前将异常记录到栈顶span中。

然后打包agent jar,测试,发现log已经记录在链路数据中:

到此,tomcat插件就编写完成,拿出去就可以监控tomcat容器接口了, 最后我再总结以下新增一个插件的四部曲:

1. 定义XXInstrumentation插件定义类,描述好增强类和方法。

2. 定义hadluo-apm-plugin.def插件定义文件,配置好XXInstrumentation。

3. 写好XXInterceptor方法环绕拦截器 。

4. 在apm-agent-core的pom中依赖插件的maven坐标。

后续插件都是这四部曲,我就不讲解很细致了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/495896.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Intent--组件通信

组件通信1 获取子活动的返回值 创建Activity时实现自动注册&#xff01;【Activity必须要注册才能使用】 默认 LinearLayout 布局&#xff0c;注意 xml 中约束布局的使用&#xff1b; 若需要更改 线性布局 只需要将标签更改为 LinearLayout 即可&#xff0c;记得 设置线性布局…

Unittest02|TestSuite、TestRunner、HTMLTestRunner、处理excel表数据、邮件接收测试结果

目录 八、测试套件TestSuite和测试运行器TestRunner 1、基本概念 2、创建和使用测试套件 3、 自动发现测试用例、创建测试套件、运行测试 4、生成html的测试报告&#xff1a;HTMLTestRunner 1️⃣导入HTMLTestRunner模块 2️⃣运行测试用例并生成html文件 九、unittest…

汽车免拆诊断案例 | 2011 款奔驰 S400L HYBRID 车发动机故障灯异常点亮

故障现象 一辆2011款奔驰 S400L HYBRID 车&#xff0c;搭载272 974发动机和126 V高压电网系统&#xff0c;累计行驶里程约为29万km。车主反映&#xff0c;行驶中发动机故障灯异常点亮。 故障诊断 接车后试车&#xff0c;组合仪表上的发动机故障灯长亮&#xff1b;用故障检测…

【Java 数据结构】面试题 02.02. 返回倒数第 k 个节点

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 目录 1. 题目 2. 解析 2.1 普通方法 2.1 快慢节点方法 3. 代码实现 3.1 普通方法 3.2 快慢节点方法 4. 小结 1. 题目 实现一种算法&#xff0c;找出单向链表…

如何在 Scrum 管理中化解团队冲突?

在Scrum管理中&#xff0c;团队协作是项目成功的关键。然而&#xff0c;团队冲突是难以避免的&#xff0c;尤其是在快速变化的敏捷环境中。如何有效处理团队冲突&#xff0c;不仅是Scrum Master需要面对的挑战&#xff0c;也是整个团队提升效率的机会。本文将围绕团队冲突的原因…

【QED】爱丽丝与混沌的无尽海

文章目录 题目题目描述输入输出格式数据范围测试样例 思路代码复杂度分析时间复杂度空间复杂度 题目 题目链接&#x1f517; 题目描述 如图所示&#xff0c;爱丽丝在一个3x3的迷宫之中&#xff0c;每个方格中标有 1 − 9 1-9 1−9各不相同的数字&#xff0c;爱丽丝可以从一格…

yii2 手动添加 phpoffice\phpexcel

1.下载地址&#xff1a;https://github.com/PHPOffice/PHPExcel 2.解压并修改文件名为phpexcel 在yii项目的vendor目录下创建一个文件夹命名为phpoffice 把phpexcel目录放到phpoffic文件夹下 查看vendor\phpoffice\phpexcel目录下会看到这些文件 3.到vendor\composer目录下…

排序算法之快速排序、归并排序

目录 快速排序归并排序的意义 快速排序 思维步骤 具体思想 测试样例解释 代码实现 归并排序 思维步骤 具体思想 测试样例解释 代码实现 快速排序归并排序的意义 快速排序和归并排序不仅仅是一种方法&#xff0c;更重要的是其作为一种算法而节省时间&#xff0c;在…

《信管通低代码信息管理系统开发平台》Windows环境安装说明

1 简介 《信管通低代码信息管理系统应用平台》提供多环境软件产品开发服务&#xff0c;包括单机、局域网和互联网。我们专注于适用国产硬件和操作系统应用软件开发应用。为事业单位和企业提供行业软件定制开发&#xff0c;满足其独特需求。无论是简单的应用还是复杂的系统&…

攻防世界web第三题file_include

<?php highlight_file(__FILE__);include("./check.php");if(isset($_GET[filename])){$filename $_GET[filename];include($filename);} ?>惯例&#xff1a; 代码审查&#xff1a; 1.可以看到include(“./check.php”);猜测是同级目录下有一个check.php文…

产品初探Devops!以及AI如何赋能Devops?

DevOps源自Development&#xff08;开发&#xff09;和Operations&#xff08;运维&#xff09;的组合&#xff0c;是一种新的软件工程理念&#xff0c;旨在打破传统软件工程方法中“开发->测试->运维”的割裂模式&#xff0c;强调端到端高效一致的交付流程&#xff0c;实…

初始 ShellJS:一个 Node.js 命令行工具集合

一. 前言 Node.js 丰富的生态能赋予我们更强的能力&#xff0c;对于前端工程师来说&#xff0c;使用 Node.js 来编写复杂的 npm script 具有明显的 2 个优势&#xff1a;首先&#xff0c;编写简单的工具脚本对前端工程师来说额外的学习成本很低甚至可以忽略不计&#xff0c;其…

Blender真实灰尘粒子动画资产预设 Dust Particles Pro V1.2

Dust Particles Pro V1.2 是一款为Blender 3.5.1及更高版本设计的实时程序化粒子资产&#xff0c;由Geometry Nodes提供支持。这款资产不需要安装&#xff0c;因为它不是一个Python插件。如果你对Blender的Geometry Nodes还不熟悉&#xff0c;那么这款资产将为你带来惊喜&#…

No.1免费开源ERP:Odoo自定义字段添加到配置页中的技术分享

文 / 开源智造&#xff08;OSCG&#xff09; Odoo亚太金牌服务 在Odoo18之中&#xff0c;配置设定于管控各类系统配置层面发挥着关键之效用&#xff0c;使您能够对软件予以定制&#xff0c;以契合您特定的业务需求。尽管 Odoo 提供了一组强劲的默认配置选项&#xff0c;然而有…

Python的安装过程和环境搭建(超详细过程)

目录 一、下载Python资源包 二、下载PyCharm资源包 三、配置Python环境 3.1 双击Python3.7.4文件&#xff08;建议右击以管理员身份打开&#xff09; 3.2 选择“Install Now”和勾选“Add Python 3.7 to Path” 3.3 出现该页面&#xff0c;进行等待 3.4 显示该页面表示…

THREE.js 入门(六) 纹理、uv坐标

一、uv坐标 相当于x、y轴&#xff0c;通过自定义uv坐标可以截取所需的纹理范围 <template><div id"container"></div> </template><script setup> import * as THREE from "three"; import { onMounted } from "vue&…

【星海随笔】删除ceph

cephadm shell ceph osd set noout ceph osd set norecover ceph osd set norebalance ceph osd set nobackfill ceph osd set nodown ceph osd set pause参考文献&#xff1a; https://blog.csdn.net/lyf0327/article/details/90294011 systemctl stop ceph-osd.targetyum re…

学习threejs,THREE.RingGeometry 二维平面圆环几何体

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.RingGeometry 圆环几…

【C语言】深入探讨 C 语言 `int` 类型大小及其跨平台影响

C 语言中 int 类型字节数的全面讲解 C 语言作为一种通用编程语言&#xff0c;其数据类型的大小由多种因素共同决定&#xff0c;而 int 类型作为最常用的整数类型之一&#xff0c;其字节数&#xff08;大小&#xff09;往往备受关注。本文将系统性地探讨 int 类型字节数的相关知…

Linux -- 互斥的底层实现

lock 和 unlock 的汇编伪代码如下&#xff1a; lock:movb $0,%alxchgb %al,mutexif(al 寄存器的内容>0)return 0;else挂起等待&#xff1b;goto lock;unlock:movb $1,mutex唤醒等待 mutex 的线程&#xff1b;return 0; 我们来理解以下上面的代码。 首先线程 1 申请锁&…