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. 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示ORMRedis的链路跟踪记录。
  3. 我们在程序启动时通过以下方法设置当前数据库缓存管理的适配器为redis。关于缓存适配器的介绍感兴趣可以参考 缓存管理-接口设计 章节。

    g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis()))
  4. ORM的操作中,需要通过Ctx方法将上下文变量传递到组件中,orm组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。
  5. ORM的操作中,这里使用Cache方法缓存查询结果到redis中,并在删除操作中也使用Cache方法清除redis中的缓存结果。关于ORM的缓存管理介绍请参考 ORM链式操作-查询缓存 章节。

效果查看

启动服务端:

启动客户端:

Jaeger上查看链路信息:

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

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

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

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

现在就没有报错了。

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

ORM链路信息

Attributes/Tags

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

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

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

Events/Process

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

当前SQL语句执行耗时,单位为ms毫秒。

Redis链路信息

Attributes/Tags

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

Events/Process

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

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











Content Menu

  • No labels

7 Comments

  1. 这个不错, 推荐这种方法, 毕竟阿里云有免费的额度

    1. 不大的话, 基本够.

  2. 请教一下, 这种http提交, 是每个并发提交一次, 还是积攒多少秒提交一次. 这种提交会不会拖累系统. 如果是异步的还好, 同步的话, ...

    1. 基本上不会出现这个情况,也可以花个百八十块去测试一下,如果都在阿里云是可以走内网的2.5G的通道。

      1. 想弄懂这个, 估计得去扒拉gf的源码了. ...

        1. 应该是扒拉 otel的代码 和gf 关系不大

          1. 啊? 这种对接阿里云的服务的, 难道本地也要安装OpenTelemetry?  我还以为是直接http提交到阿里云