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

HTTP+DB+Redis+Logging

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

客户端

package main

import (
    "github.com/gogf/gf/contrib/trace/otlphttp/v2"
    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/net/gtrace"
    "github.com/gogf/gf/v2/os/gctx"
)

const (
    serviceName = "otlp-http-client"
    endpoint    = "tracing-analysis-dc-hz.aliyuncs.com"
    path        = "adapt_******_******/api/otlp/traces" )

func main() {
    var ctx = gctx.New()
    shutdown, err := otlphttp.Init(serviceName, endpoint, path)
    if err != nil {
        g.Log().Fatal(ctx, err)
    }
    defer shutdown()

    StartRequests()
}

func StartRequests() {
    ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests")
    defer span.End()

    var (
        err    error
        client = g.Client()
    )
    // Add user info.
    var insertRes = struct {
        ghttp.DefaultHandlerResponse
        Data struct{ Id int64 } `json:"data"`
    }{}
    err = client.PostVar(ctx, "http://127.0.0.1:8199/user/insert", g.Map{
        "name": "john",
    }).Scan(&insertRes)
    if err != nil {
        panic(err)
    }
    g.Log().Info(ctx, "insert result:", insertRes)
    if insertRes.Data.Id == 0 {
        g.Log().Error(ctx, "retrieve empty id string")
        return
    }

    // Query user info.
    var queryRes = struct {
        ghttp.DefaultHandlerResponse
        Data struct{ User gdb.Record } `json:"data"`
    }{}
    err = client.GetVar(ctx, "http://127.0.0.1:8199/user/query", g.Map{
        "id": insertRes.Data.Id,
    }).Scan(&queryRes)
    if err != nil {
        panic(err)
    }
    g.Log().Info(ctx, "query result:", queryRes)

    // Delete user info.
    var deleteRes = struct {
        ghttp.DefaultHandlerResponse
    }{}
    err = client.PostVar(ctx, "http://127.0.0.1:8199/user/delete", g.Map{
        "id": insertRes.Data.Id,
    }).Scan(&deleteRes)
    if err != nil {
        panic(err)
    }
    g.Log().Info(ctx, "delete result:", deleteRes)
}

客户端代码简要说明:

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

服务端

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/gogf/gf/contrib/trace/otlphttp/v2"
    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
)

type cTrace struct{}

const (
    serviceName = "otlp-http-client"
    endpoint    = "tracing-analysis-dc-hz.aliyuncs.com"
    path        = "adapt_******_******/api/otlp/traces" )

func main() {
    var ctx = gctx.New()
    shutdown, err := otlphttp.Init(serviceName, endpoint, path)
    if err != nil {
        g.Log().Fatal(ctx, err)
    }
    defer shutdown()

    // Set ORM cache adapter with redis.
    g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))

    // Start HTTP server.
    s := g.Server()
    s.Use(ghttp.MiddlewareHandlerResponse)
    s.Group("/", func(group *ghttp.RouterGroup) {
        group.ALL("/user", new(cTrace))
    })
    s.SetPort(8199)
    s.Run()
}

type InsertReq struct {
    Name string `v:"required#Please input user name."`
}
type InsertRes struct {
    Id int64
}

// Insert is a route handler for inserting user info into database.
func (c *cTrace) Insert(ctx context.Context, req *InsertReq) (res *InsertRes, err error) {
    result, err := g.Model("user").Ctx(ctx).Insert(req)
    if err != nil {
        return nil, err
    }
    id, _ := result.LastInsertId()
    res = &InsertRes{
        Id: id,
    }
    return
}

type QueryReq struct {
    Id int `v:"min:1#User id is required for querying"`
}
type QueryRes struct {
    User gdb.Record
}

// Query is a route handler for querying user info. It firstly retrieves the info from redis,
// if there's nothing in the redis, it then does db select.
func (c *cTrace) Query(ctx context.Context, req *QueryReq) (res *QueryRes, err error) {
    one, err := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
        Duration: 5 * time.Second,
        Name:     c.userCacheKey(req.Id),
        Force:    false,
    }).WherePri(req.Id).One()
    if err != nil {
        return nil, err
    }
    res = &QueryRes{
        User: one,
    }
    return
}

type DeleteReq struct {
    Id int `v:"min:1#User id is required for deleting."`
}
type DeleteRes struct{}

// Delete is a route handler for deleting specified user info.
func (c *cTrace) Delete(ctx context.Context, req *DeleteReq) (res *DeleteRes, err error) {
    _, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
        Duration: -1,
        Name:     c.userCacheKey(req.Id),
        Force:    false,
    }).WherePri(req.Id).Delete()
    if err != nil {
        return nil, err
    }
    return
}

func (c *cTrace) userCacheKey(id int) string {
    return fmt.Sprintf(`userInfo:%d`, id)
}

服务端代码简要说明:

  1. 首先,客户端也是需要通过jaeger.Init方法初始化Jaeger。
  2. 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示ORM和Redis的链路跟踪记录。
  3. 我们在程序启动时通过以下方法设置当前数据库缓存管理的适配器为redis。关于缓存适配器的介绍感兴趣可以参考 缓存管理-接口设计 章节。
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

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

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

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

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

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

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

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

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

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

现在就没有报错了。

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

ORM链路信息

Attributes/Tags

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

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

可以看到这里的span.kind是internal,也就是之前介绍过的方法内部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.ExecContext和DB.QueryContext,分别代表写操作和读操作。
db.execution.cost 当前SQL语句执行耗时,单位为ms毫秒。

Redis链路信息

Attributes/Tags

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

Attribute/Tag 说明
redis.host Redis连接地址。
redis.port Redis连接端口。
redis.db Redis操作db。

Events/Process

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

Event/Log 说明
redis.execution.command Redis执行指令。
redis.execution.arguments Redis执行指令参数。
redis.execution.cost Redis执行指令执行耗时,单位为ms毫秒。