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 files

PART1. 编写测试用例

想要完全的测试一个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如下:

问题1-请求的span没有和handleFunc内的span关联

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

firstLayerSpan的父子关系

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 修复结果

修复结果
修复后的span父子关系

PART5. 缺陷

缺陷-span的标签中没有响应码

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

5.1 记录响应码的代码在哪里加?

context中没有响应码

加肯定是在中间件中调完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.RespDataContext.RespStatusCode中读取到数据的

5.2.2 实现

a. 修改响应方法

这里所有写入到响应的地方都要改成写入到RespDataRespStatusCode这两个字段上:

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:

拿到了状态码但是没拿到host

最后一个bug:没拿到Host

这里是因为取Host时字段写错了,一个比较好修的Bug:

middlewares/open_telemetry/middlewareBuilder.go:

获取到host和响应码的span

至此,链路追踪中间件开发完成

附录

TODO: 我在写的时候,想过将http.ResponseWriter直接设置为私有字段,有时间了我自己试一下

Last updated