HTTP+DB+Redis+Logging

我们再来看一个相对完整一点的例子,包含几个常用核心组件的链路跟踪示例,示例代码地址: https://github.com/gogf/gf/tree/master/example/trace/http_with_db

客户端

  1. package main
  2. import (
  3. "github.com/gogf/gf/contrib/trace/otlphttp/v2"
  4. "github.com/gogf/gf/v2/database/gdb"
  5. "github.com/gogf/gf/v2/frame/g"
  6. "github.com/gogf/gf/v2/net/ghttp"
  7. "github.com/gogf/gf/v2/net/gtrace"
  8. "github.com/gogf/gf/v2/os/gctx"
  9. )
  10. const (
  11. serviceName = "otlp-http-client"
  12. endpoint = "tracing-analysis-dc-hz.aliyuncs.com"
  13. path = "adapt_******_******/api/otlp/traces" )
  14. func main() {
  15. var ctx = gctx.New()
  16. shutdown, err := otlphttp.Init(serviceName, endpoint, path)
  17. if err != nil {
  18. g.Log().Fatal(ctx, err)
  19. }
  20. defer shutdown()
  21. StartRequests()
  22. }
  23. func StartRequests() {
  24. ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests")
  25. defer span.End()
  26. var (
  27. err error
  28. client = g.Client()
  29. )
  30. // Add user info.
  31. var insertRes = struct {
  32. ghttp.DefaultHandlerResponse
  33. Data struct{ Id int64 } `json:"data"`
  34. }{}
  35. err = client.PostVar(ctx, "http://127.0.0.1:8199/user/insert", g.Map{
  36. "name": "john",
  37. }).Scan(&insertRes)
  38. if err != nil {
  39. panic(err)
  40. }
  41. g.Log().Info(ctx, "insert result:", insertRes)
  42. if insertRes.Data.Id == 0 {
  43. g.Log().Error(ctx, "retrieve empty id string")
  44. return
  45. }
  46. // Query user info.
  47. var queryRes = struct {
  48. ghttp.DefaultHandlerResponse
  49. Data struct{ User gdb.Record } `json:"data"`
  50. }{}
  51. err = client.GetVar(ctx, "http://127.0.0.1:8199/user/query", g.Map{
  52. "id": insertRes.Data.Id,
  53. }).Scan(&queryRes)
  54. if err != nil {
  55. panic(err)
  56. }
  57. g.Log().Info(ctx, "query result:", queryRes)
  58. // Delete user info.
  59. var deleteRes = struct {
  60. ghttp.DefaultHandlerResponse
  61. }{}
  62. err = client.PostVar(ctx, "http://127.0.0.1:8199/user/delete", g.Map{
  63. "id": insertRes.Data.Id,
  64. }).Scan(&deleteRes)
  65. if err != nil {
  66. panic(err)
  67. }
  68. g.Log().Info(ctx, "delete result:", deleteRes)
  69. }

客户端代码简要说明:

  1. 首先,客户端也是需要通过 jaeger.Init 方法初始化 Jaeger
  2. 在本示例中,我们通过HTTP客户端向服务端发起了 3 次请求:
  3. /user/insert 用于新增一个用户信息,成功后返回用户的ID。
  4. /user/query 用于查询用户,使用前一个接口返回的用户ID。
  5. /user/delete 用于删除用户,使用之前接口返回的用户ID。

服务端

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. "github.com/gogf/gf/contrib/trace/otlphttp/v2"
  7. "github.com/gogf/gf/v2/database/gdb"
  8. "github.com/gogf/gf/v2/frame/g"
  9. "github.com/gogf/gf/v2/net/ghttp"
  10. "github.com/gogf/gf/v2/os/gcache"
  11. "github.com/gogf/gf/v2/os/gctx"
  12. )
  13. type cTrace struct{}
  14. const (
  15. serviceName = "otlp-http-client"
  16. endpoint = "tracing-analysis-dc-hz.aliyuncs.com"
  17. path = "adapt_******_******/api/otlp/traces" )
  18. func main() {
  19. var ctx = gctx.New()
  20. shutdown, err := otlphttp.Init(serviceName, endpoint, path)
  21. if err != nil {
  22. g.Log().Fatal(ctx, err)
  23. }
  24. defer shutdown()
  25. // Set ORM cache adapter with redis.
  26. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))
  27. // Start HTTP server.
  28. s := g.Server()
  29. s.Use(ghttp.MiddlewareHandlerResponse)
  30. s.Group("/", func(group *ghttp.RouterGroup) {
  31. group.ALL("/user", new(cTrace))
  32. })
  33. s.SetPort(8199)
  34. s.Run()
  35. }
  36. type InsertReq struct {
  37. Name string `v:"required#Please input user name."`
  38. }
  39. type InsertRes struct {
  40. Id int64
  41. }
  42. // Insert is a route handler for inserting user info into database.
  43. func (c *cTrace) Insert(ctx context.Context, req *InsertReq) (res *InsertRes, err error) {
  44. result, err := g.Model("user").Ctx(ctx).Insert(req)
  45. if err != nil {
  46. return nil, err
  47. }
  48. id, _ := result.LastInsertId()
  49. res = &InsertRes{
  50. Id: id,
  51. }
  52. return
  53. }
  54. type QueryReq struct {
  55. Id int `v:"min:1#User id is required for querying"`
  56. }
  57. type QueryRes struct {
  58. User gdb.Record
  59. }
  60. // Query is a route handler for querying user info. It firstly retrieves the info from redis,
  61. // if there's nothing in the redis, it then does db select.
  62. func (c *cTrace) Query(ctx context.Context, req *QueryReq) (res *QueryRes, err error) {
  63. one, err := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
  64. Duration: 5 * time.Second,
  65. Name: c.userCacheKey(req.Id),
  66. Force: false,
  67. }).WherePri(req.Id).One()
  68. if err != nil {
  69. return nil, err
  70. }
  71. res = &QueryRes{
  72. User: one,
  73. }
  74. return
  75. }
  76. type DeleteReq struct {
  77. Id int `v:"min:1#User id is required for deleting."`
  78. }
  79. type DeleteRes struct{}
  80. // Delete is a route handler for deleting specified user info.
  81. func (c *cTrace) Delete(ctx context.Context, req *DeleteReq) (res *DeleteRes, err error) {
  82. _, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
  83. Duration: -1,
  84. Name: c.userCacheKey(req.Id),
  85. Force: false,
  86. }).WherePri(req.Id).Delete()
  87. if err != nil {
  88. return nil, err
  89. }
  90. return
  91. }
  92. func (c *cTrace) userCacheKey(id int) string {
  93. return fmt.Sprintf(`userInfo:%d`, id)
  94. }

服务端代码简要说明:

  1. 首先,客户端也是需要通过 jaeger.Init 方法初始化 Jaeger
  2. 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示 ORMRedis 的链路跟踪记录。
  3. 我们在程序启动时通过以下方法设置当前数据库缓存管理的适配器为 redis。关于缓存适配器的介绍感兴趣可以参考 缓存管理-接口设计 章节。
  1. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))
  1. ORM 的操作中,需要通过 Ctx 方法将上下文变量传递到组件中, orm 组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。
  2. ORM 的操作中,这里使用 Cache 方法缓存查询结果到 redis 中,并在删除操作中也使用 Cache 方法清除 redis 中的缓存结果。关于 ORM 的缓存管理介绍请参考 ORM链式操作-查询缓存 章节。

效果查看

启动服务端:

链路跟踪-HTTP示例-数据操作 - 图1

启动客户端:

链路跟踪-HTTP示例-数据操作 - 图2

Jaeger 上查看链路信息:

链路跟踪-HTTP示例-数据操作 - 图3

可以看到,这次请求总共产生了 14span,其中客户端有 4span,服务端有 10span,每一个 span 代表一个链路节点。不过,我们注意到,这里产生了 3errors。我们点击详情查看什么原因呢。

链路跟踪-HTTP示例-数据操作 - 图4

我们看到好像所有的 redis 操作都报错了,随便点击一个 redis 的相关 span,查看一下详情呢:

链路跟踪-HTTP示例-数据操作 - 图5

原来是 redis 连接不上报错了,这样的话所有的 orm 缓存功能都失效了,但是可以看到并没有影响接口逻辑,只是所有的查询都走了数据库。这个报错是因为我本地忘了打开 redis server,我赶紧启动一下本地的 redis server,再看看效果:

链路跟踪-HTTP示例-数据操作 - 图6

再把上面的客户端运行一下,查看 jaeger

链路跟踪-HTTP示例-数据操作 - 图7

链路跟踪-HTTP示例-数据操作 - 图8

现在就没有报错了。

HTTP Client&ServerLogging 组件在之前已经介绍过,因此这里我们主要关注 ormredis 组件的链路跟踪信息。

ORM链路信息

Attributes/Tags

我们随便点开一个 ORM 链路 Span,看看 Attributes/Tags 信息:

链路跟踪-HTTP示例-数据操作 - 图9

可以看到这里的 span.kindinternal,也就是之前介绍过的方法内部 span 类型。这里很多 Tags 在之前已经介绍过,因此这里主要介绍关于数据库相关的 Tags

Attribute/Tag说明
<br /> db.type<br />数据库连接类型。如 mysql, mssql, pgsql 等等。
db.link数据库连接信息。其中密码字段被自动隐藏。
db.group在配置文件中的数据库分组名称。

Events/Process

链路跟踪-HTTP示例-数据操作 - 图10

Event/Log说明
db.execution.sql执行的具体 SQL 语句。由于ORM底层是预处理,该语句为方便查看自动拼接而成,仅供参考。
db.execution.type执行的 SQL 语句类型。常见为 DB.ExecContextDB.QueryContext,分别代表写操作和读操作。
db.execution.cost当前 SQL 语句执行耗时,单位为 ms 毫秒。

Redis链路信息

Attributes/Tags

链路跟踪-HTTP示例-数据操作 - 图11

Attribute/Tag说明
<br /> redis.host<br />Redis 连接地址。
redis.portRedis 连接端口。
redis.dbRedis 操作 db

Events/Process

链路跟踪-HTTP示例-数据操作 - 图12

Event/Log说明
redis.execution.commandRedis 执行指令。
redis.execution.argumentsRedis 执行指令参数。
redis.execution.costRedis 执行指令执行耗时,单位为 ms 毫秒。