7.03 Middleware-OpenTelemetry测试
本节课工程结构如下:
(base) yanglei@yuanhong 03-tracingTest % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│ ├── access_log
│ │ ├── accessLog.go
│ │ ├── accessLog_test.go
│ │ └── middlewareBuilder.go
│ └── open_telemetry
│ └── middlewareBuilder.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go
3 directories, 21 filesPART1. 编写测试用例
想要完全的测试一个tracer是否正确存储其实是比较困难的,这里不太好办的点在于:你不太容易把span从OpenTelemetry中取出来看,进而确保这个span上报到了zipkin或jeager中.所以这里的测试思路只能是:写个HandleFunc,在这个HandleFunc中记录一些span,最后去zipkin或jeager中查看.
就像我们平时对一个CRUD的测试一样,写一个INSERT语句,测试之后只要看数据是否落盘即可.
1.1 创建Middleware和Server
middlewares/open_telemetry/middleware_test.go:
这里要注意,各层级的span之间是有父子关系的,创建的时候必然是先创建父span,再基于父span创建子span;关闭的时候则相反,需要先关闭子span再关闭父span.如果关闭的时候先关父span,则子span会随之关闭.这里我们为了观察各层级span之间的父子关系,就不把关闭操作写的太复杂了.
1.2 初始化Tracing
1.2.1 初始化zipkin
middlewares/open_telemetry/middleware_test.go:
1.2.2 初始化jeager
middlewares/open_telemetry/middleware_test.go:
注:以上两段代码都是直接粘过来的,因为我觉得这种代码意义不是很大,而且我确实也明白不了这里边都是啥意思,有需要直接GPT查应该就行
1.3 设置OpenTelemetry的Tracer Provider
实际上就是在HandleFunc中调用以上两个函数中的任意一个即可,因为这2个函数中都调用了otel.SetTracerProvider(),设置了全局的 Tracer提供器.
middlewares/open_telemetry/middleware_test.go:
PART2. 启动zipkin
middlewares/open_telemetry/docker-compose.yaml:
PART3. 测试
请求http://localhost:8092/user后,查看zipkin如下:

这里是不太符合预期的,我们是希望first_layer_1和first_layer_2挂在/user下的.

PART4. 修Bug
4.1 修复层级关系不正确的问题
4.1.1 原因

换言之,在HandleFunc中拿到的ctx.Req.Context,和在中间件中创建的、和span创建了绑定关系的context.Context不是一个Context
4.1.2 修复
middlewares/open_telemetry/middlewareBuilder.go:
这里需要注意的地方是:http.Request.WithContext()方法的实现原理是复制原来的http.Request实例到一个新创建的http.Request实例上,然后修改这个新创建的http.Request实例的ctx字段,因此性能会比较差
如果要是想直接创建一个http.Request实例,则应该使用http.NewRequestWithContext()函数.
当然也有其他方案,比如给web.Context加一个context.Context,所有关于请求的context.Context都读写这个字段:
在Context上添加字段:
context.go:
中间件中写入到Context.Ctx:
middlewares/open_telemetry/middlewareBuilder.go:
HandleFunc中使用Context.Ctx:
middlewares/open_telemetry/middleware_test.go:
但这个方案其实问题也比较严重,虽然它没有太大的性能问题,但它造成了一个令使用者困惑的问题:你这个框架的Context中有2个地方提供了context.Context:
Context.Req.Context()Context.Ctx
使用者也会疑惑:我到底应该用哪个?
所以最好还是别让使用者有这种选择权了,一则对于我们也不利,我们要维护2个地方;再则对于使用者也不利,使用者也会产生疑惑
4.1.3 修复结果


PART5. 缺陷

从上图中可以看出,我们的span没有记录响应码.这是一个需要修复的缺陷
5.1 记录响应码的代码在哪里加?

加肯定是在中间件中调完next()加,可问题是:现在Context里没记录响应码
5.2 Context中记录响应码
5.2.1 实现思路
这里我们就顺便把响应数据也记录下来了,因为后边设计其他中间件时,肯定还会遇到需要响应数据的中间件
首先,不要想着从http.ResponseWriter(ctx.Resp)接口中直接拿,因为它没有提供获取响应码的方法
其次,类型转换也不行.这里我们假定你有一个MyResponseWriter类,实现了http.ResponseWriter接口,可能你会想到如下的代码:

还是不行:因为你在这里无法确定你要把一个什么类型转换为你自己实现的MyResponseWriter.有可能是http包内置的类,还有可能是使用者通过写中间件的方式修改过的类,你根本无法确保这里的类型转换一定会成功
那最后只剩下1条路了:单独在Context上创建字段记录响应码和相应数据
context.go:
当然,这个方案也是有问题的:如果用户还是使用ctx.Resp.Write()和ctx.Resp.WriteHeader()这种关于http.ResponseWriter的API,那么你的中间件是无法从Context.RespData和Context.RespStatusCode中读取到数据的
5.2.2 实现
a. 修改响应方法
这里所有写入到响应的地方都要改成写入到RespData和RespStatusCode这两个字段上:
context.go:
httpServer.go:
到目前为止,我们还没有真的将数据刷到响应中,接下来解决这个问题
b. 写入数据到响应
这一步的问题在于:该在什么事件节点上把RespJSON()和server()中提供的响应状态码和响应数据,刷到响应上呢?
答案比较明显:在所有的中间件对响应状态码和响应数据的读写操作(中间件进行写响应就相当于是篡改响应的操作了)都完成后,换言之即响应返回给客户端之前,将把RespJSON()和server()中提供的响应状态码和响应数据刷到响应上
注意我们中间件的顺序问题:中间件中在next(ctx)之前的代码,是后添加的中间件先执行;中间件中在next(ctx)之后的代码,是先添加的中间件先执行
因此应该在中间件链的"头部"(这里我们将先添加的中间件称为"尾部",后添加的中间件称为"头部")添加一个在next(ctx)之后执行的中间件:
httpServer.go:
c. 写入失败的错误处理
还有问题:如果刷响应失败了怎么办?
注意你是不能直接用log.Fatal()之类的方法的,因为有可能框架的使用者是不希望直接把日志打印到控制台上的
step1. 定义日志函数
httpServer.go:
step2. 处理错误并调用日志函数
httpServer.go:
step3. 将中间件的处理抽象成一个方法
httpServer.go:
注意这里要先刷响应码再刷响应数据
step4. 初始化
HTTPServer时,初始化日志函数
这里如果使用者需要注入自定义的日志函数,仅需传入一个ServerOption即可.所以这里要做的只是提供一个默认的日志函数:
httpServer.go:
PART6. 修改测试用例
注意现在就不能再在HandleFunc中直接调用http.ResponseWriter的方法了,因为那样会导致中间件拿不到响应码的
middlewares/open_telemetry/middleware_test.go:

最后一个bug:没拿到Host
这里是因为取Host时字段写错了,一个比较好修的Bug:
middlewares/open_telemetry/middlewareBuilder.go:

至此,链路追踪中间件开发完成
附录
TODO: 我在写的时候,想过将http.ResponseWriter直接设置为私有字段,有时间了我自己试一下
Last updated