概述
为了增强 Golang 程序的可观测性,方便定位问题,我们往往会在代码中集成链路追踪 (tracing) 的能力,Jaeger 是当今比较主流的选择,而 tracing 相关的 API 如今都抽象到了 OpenTelemetry 项目中,涵盖各种实现,也包括 Jaeger 在内。
req 是一款功能非常强大且易用的 Golang HTTP 请求库,利用 req 强大的中间件能力,可以轻松为我们涉及 HTTP 调用的代码统一集成链路追踪的能力,且能以最少的代码量进行扩展。
本文将给出一个可运行的程序示例:输入一个 GitHub 用户名,展示用户的简短介绍,包含名字、网站地址以及该用户下的最火开源项目与 star 数量,期间涉及的函数与 API 调用链路追踪信息均上报至 Jaeger,进行可视化展示。
主要包含以下特点:
内置一个基于 req 封装的 GitHub SDK。
SDK 中利用 req 的 与 ,统一处理 API 异常,对接 API 的实现函数无需关心错误处理。
SDK 支持传入 OpenTelemetry 的 Tracer 来开启链路追踪,利用 req 的 Client 中间件能力,在请求前创建 trace span,并记录请求与响应的详细信息到 span 中(URL、Method、请求头、请求体、响应状态码、响应头、响应体等),在响应结束后自动终止 span。
在调用 SDK 的上层函数也使用 trace,层层传递,在 Jaeger UI 上可查看完整且非常详细的调用链路详情。
初始化项目
首先创建一个目录,使用 初始化工程:
封装支持 Tracing 的 GitHub SDK
在项目根目录下面创建一个名为 的目录,作为内置的 GitHub SDK 的 package,在里面创建源文件 ,写入代码:
使用 结构体作为 GitHub 的客户端,也是 SDK 的核心结构体,内置了一个 。
分别使用 与 为 GitHub 所有 API 请求设置统一的 请求头与 URL 前缀。
GitHub API 响应的错误格式是统一的,使用 告知 req 如果响应了错误(状态码大于等于400),则自动将响应体 Unmarshal 到 结构体的对象中。
结构体实现了 go 的 error 接口,将 API 层面的错误信息转换成可读的字符串。
在 中设置 ,检测到 API 响应错误时,将其写入到 ,自动会将其作为 go error 抛给上层的调用方。
在 中设置 ,为所有请求开启请求级别的 dump (暂存到内存,不打印出来),若遇到底层错误(如超时、dns 解析失败、Unmarshal 失败),或者收到未知的状态码(小于200),在 中尽可能将有助于定位问题的信息(dump 内容)记录到 error,写入 以便抛给上层的调用方。
下面为 增加 Tracing 的能力:
在 中传入 OpenTelemetry 的 Tracer 来开启 Tracing 能力。
调用 中内置的 的 添加 Client 中间件,确保将 返回的 resp 和 err 最终返回给上层。该行代码之前是发起请求前,可记录请求信息,之后是收到响应后,可记录响应信息。
在中间件实现函数里,为每个请求创建一个 trace span,从 context 中获取 API 名称作为 span 名称,如果 context 中有 parant span,当前 span 也会自动成为其 child span。
使用 确保在响应结束后再结束 span,以便 tracing 能够正确统计耗时。
将请求与响应的详细信息全都记录到 span 中,如 URL、Method、请求头、请求体、响应状态码、响应头、响应体等。
如果检测到 error,也记录到 span 中并设置 span 的 error 状态。
下面开始对接 GitHub API,第一个实现的是获取 GitHub 用户信息的 API,方法命名为 :
链路追踪的 span 都是通过 context 传递,每个方法的第一个参数都用 context。
利用链式调用构造 Request, 表示创建一个 请求,并传入 API 路径(之前 已设置所有请求的 URL 前缀,这里就可以省略前缀只写路径), 路径中还有 路径参数 (REST 风格 API),使用 传入。
响应体格式是 结构体,直接将返回参数中的空指针变量的地址传入 ,表示如果没有异常,自动创建一个该结构体类型的对象,并让指针变量指向该结构体,这样都不需要自己事先初始化结构体,减少代码量。
利用公共函数 将 API 名称放入 context,然后调用 发起请求时,将 context 传进去,以便让 Client 中间件能够获取到 API 名称并自动将其作为 span 名称。
会返回 ,任何情况它都不为 nil,如果请求过程中返回了 error,会记录到其 字段,将其赋值给返回参数的 以便 error 能够层层传递上去。
下面再来增加一个获取指定用户代码仓库列表的 API :
该 API 支持分页,需要传入 username 和 page。
page 是整数类型,需要将其入查询参数,使用 传入所有查询参数,无需提前转成字符串。
其余与上一个 API 实现类似。
可以看到,后续我们每次对接新的 API 都变得非常轻松,因为利用了 req 的中间件能力,对异常与链路追踪都进行了统一处理,对接 API 时,只需传入 API 必要的参数与响应体结构类型即可,没有一点多余的代码,非常直观和简洁。
好了,作为示例我们就只对接这两个 API 就够了,我们还可以再为 Client 增加一些实用的小功能:
如果是匿名用户调用 GitHub API,会有限频,可以使用 token 来避免被限频,增加 以支持为所有请求带上认证的 personal access token。
增加 以支持 debug 能力,开启 debug 时,将打印 req 的 debug 日志以及原始的请求与响应内容。
至此,我们的 GitHub SDK 封装完成。
程序示例
下面,正式开始写可运行的示例程序。
在项目根目录下创建 :
定义 作为本服务的标识 (通常每个程序都是一个服务,上报 tracing 数据时,需标识服务名),这里就定义为 。
本示例程序需要调用 GitHub API 进行查询,使用前面我们封装的 GitHub SDK 作为 client,这里定义一个全局 变量,内部函数直接使用该 client 进行调用。
使用 OpenTelemetry 进行链路追踪,需要创建一个 ,这里我们定义 函数来创建包含 Jaeger 实现的 :
使用 自定义 Jaeger 地址,默认使用本地测试的地址。
传入 以便在 tracing 数据对本服务进行标识。
下面来写查询用户信息的主要函数 :
需传入一个 username,以便查询指定 GitHub 用户的信息。
在函数开头创建一个名为 的 root span,作为链路追踪的初始 span。
在 span 中记录查询相关信息,包含查询的 username 以及查询到的昵称、blog 地址(使用 GetUserProfile 接口),也包含该用户最火的开源项目及其 star 数量(使用 ListUserRepo 接口并进行计算对比得出)。
在函数末尾打印最终查询到的信息到控制台。
其中计算用户最火开源项目及其 star 数量由单独的 函数来实现,该函数也有对应的 span。
主要的实现函数准备就绪,现在我们来写 main 函数:
调用 创建一个 ,并使用 设置到全局共享,以便前面其它函数调用 能够使用此 provider 来创建与获取 tracer。
调用 为全局的 进行初始化。
判断环境变量,如果 则开启 Debug,如果提供 则将其设置给所有请求。
使用 来为 GitHub 的 Client 启用 Tracing 能力,用名为 的 tracer 标识 SDK 中产生的 tracing 信息。
处理 和 信号以实现优雅终止,在程序退出前关闭 ,确保 trace 数据上报完再退出 (如果程序不是常驻运行,可以在 main 函数中用 defer 语句关闭 )。
主体是一个 for 死循环: 获取用户输入的 username,然后调用 查询并展示用户信息。
大功告成,下面我们来运行一下看看效果。
运行与效果
首先按照 Jaeger 官方文档 Getting Started 在本地启动一个 Jaeger。
然后在项目根目录运行 运行程序,输入一个 GitHub 用户名(如 ),不出意外的话,会自动展示该用户的简短介绍:
然后使用浏览器进入 Jaeger UI 界面(http://127.0.0.1:16686/)来查看 Tracing 详情:
可以清晰的看到函数调用链路与耗时信息:
调用两次是因为分页查询用户 repo 时一页没查询完,分成了两次查询。
点进 的 span 详情,可以看到我们在函数内记录的查询与结果信息:
再点进 这个 SDK 产生的 span 详情,可以看到我们在中间件统一记录的 URL、Method、请求头、响应状态码、响应头、响应体等信息全都在这里,非常详细:
不断输入其它 username 测试,经过多次后可能会因 GitHub 的 API 限频导致异常:
检查下 Jaeger UI,可以看到很详细很显眼的错误信息:
此时,你可以将你的 GitHub 账号 personal access token 写到环境变量来避免被限频:
尝试输入一个不存在的用户:
检查下 Jaeger UI,同样的也可以看到详细的错误信息:
如果断开公网测试,可能会报 dns 解析失败的错:
或者连接超时的错:
完整代码
本文涉及的完整代码已放入 req 官方 examples 下的 opentelemetry-jaeger-tracing 目录。
总结
如果业务程序中需要调用其它服务的 API,我们可以利用 req 强大的中间件能力,统一处理所有请求的异常,统一记录所有请求详细信息到 Tracing 系统,写出健壮、可观测性强且极易扩展的 SDK 与业务代码。