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次请求:
    1. /user/insert 用于新增一个用户信息,成功后返回用户的ID。
    2. /user/query 用于查询用户,使用前一个接口返回的用户ID。
    3. /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()))
  4. ORM的操作中,需要通过Ctx方法将上下文变量传递到组件中,orm组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。

  5. 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说明
db.type数据库连接类型。如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说明
redis.hostRedis连接地址。
redis.portRedis连接端口。
redis.dbRedis操作db

Events/Process

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

Event/Log说明
redis.execution.commandRedis执行指令。
redis.execution.argumentsRedis执行指令参数。
redis.execution.cost

Redis执行指令执行耗时,单位为ms毫秒。