Golang實戰 XORM搭配OpenTracing+Jaeger鏈路監控讓SQL執行一覽無遺

系統環境

go version go1.14.3 windows/amd64

⭐ 項目Gitee源碼

一、Docker運行JaegerTracing-All-In-One鏡像

Docker命令

docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 14250:14250 -p 9411:9411 jaegertracing/all-in-one:1.18

瀏覽器訪問localhost:16686,可以看到JaegerUI界面,如下所示:
在這裏插入圖片描述

至此,以內存作爲數據寄存方式的OpenTracing+Jaeger服務成功運行。

二、GoModule安裝Xorm、OpenTracing和Jaeger

Xorm - 需要 1.0 版本及以上才能支持傳遞Context上下文

go get xorm.io/xorm

OpenTracing和Jaeger - 只需要安裝Jaeger-Client就會依賴Opentracing

go get github.com/uber/jaeger-client-go

三、初始化Opentracing和Jaeger

func initJaeger() (closer io.Closer, err error) {
	// 根據配置初始化Tracer 返回Closer
	tracer, closer, err := (&config.Configuration{
		ServiceName: "xormWithTracing",
		Disabled:    false,
		Sampler: &config.SamplerConfig{
			Type: jaeger.SamplerTypeConst,
			// param的值在0到1之間,設置爲1則將所有的Operation輸出到Reporter
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,
			LocalAgentHostPort: "localhost:6831",
		},
	}).NewTracer()
	if err != nil {
		return
	}

	// 設置全局Tracer - 如果不設置將會導致上下文無法生成正確的Span
	opentracing.SetGlobalTracer(tracer)
	return
}

在這裏插入圖片描述

四、通過日誌模塊將鏈路監控侵入到XORM執行邏輯中

  1. XORM通過其中Log包裏面的ContextLogger接口定義了它需要的日誌實例是怎麼樣的,我們可以在外部實現該接口就可以將鏈路監控的邏輯侵入到XORM的執行過程
// xorm.io\[email protected]\log
// ContextLogger represents a logger interface with context
type ContextLogger interface {
	SQLLogger

	Debugf(format string, v ...interface{})
	Errorf(format string, v ...interface{})
	Infof(format string, v ...interface{})
	Warnf(format string, v ...interface{})

	Level() LogLevel
	SetLevel(l LogLevel)

	ShowSQL(show ...bool)
	IsShowSQL() bool
}
  1. 實現接口的代碼比較長,這裏只截取重要部分,如果有需要可以到源碼中查看
// ---> 請注意import
// 因爲我們需要多個名爲Log的包
// 所以我們需要手動重命名
import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/opentracing/opentracing-go"
	tracerLog "github.com/opentracing/opentracing-go/log"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"github.com/uber/jaeger-client-go/log/zap"
	zap2 "go.uber.org/zap"
	"io"
	"xorm.io/xorm"
	xormLog "xorm.io/xorm/log"
)


type CustomCtxLogger struct {
	logger  *zap.Logger
	level   xormLog.LogLevel
	showSQL bool
	
	// ---> 這裏span是從上下文中創建的
	// 我們不能將創建的span寫回到上下文
	// 所以只能在Logger中維護一個span
	span    opentracing.Span
}

// BeforeSQL implements ContextLogger
func (l *CustomCtxLogger) BeforeSQL(ctx xormLog.LogContext) {
	// ----> 重頭戲在這裏,需要從Context上下文中創建一個新的Span來對SQL執行進行鏈路監控
	l.span, _ = opentracing.StartSpanFromContext(ctx.Ctx, "XORM SQL Execute")
}

// AfterSQL implements ContextLogger
func (l *CustomCtxLogger) AfterSQL(ctx xormLog.LogContext) {
	// defer結束掉span
	defer l.span.Finish()

	// 原本的SimpleLogger裏面會獲取一次SessionId
	var sessionPart string
	v := ctx.Ctx.Value("__xorm_session_id")
	if key, ok := v.(string); ok {
		sessionPart = fmt.Sprintf(" [%s]", key)
		l.span.LogFields(tracerLog.String("session_id", sessionPart))
	}

	// 將Ctx中全部的信息寫入到Span中
	l.span.LogFields(tracerLog.String("SQL", ctx.SQL))
	l.span.LogFields(tracerLog.Object("args", ctx.Args))
	l.span.SetTag("execute_time", ctx.ExecuteTime)

	if ctx.ExecuteTime > 0 {
		l.logger.Infof("[SQL]%s %s %v - %v", sessionPart, ctx.SQL, ctx.Args, ctx.ExecuteTime)
	} else {
		l.logger.Infof("[SQL]%s %s %v", sessionPart, ctx.SQL, ctx.Args)
	}
}

// 下面還有一些比較簡單的接口實現,有興趣的可以看源碼
  1. 重寫XORM引擎創建方法
func NewEngine() (engine *xorm.Engine, err error) {
	// XORM創建引擎
	engine, err = xorm.NewEngine("mysql", "test:test@/test?charset=utf8mb4")
	if err != nil {
		return
	}

	// 創建自定義的日誌實例
	_l, err := zap2.NewDevelopment()
	if err != nil {
		return
	}

	// 將日誌實例設置到XORM的引擎中
	engine.SetLogger(&CustomCtxLogger{
		logger:  zap.NewLogger(_l),
		level:   xormLog.LOG_DEBUG,
		showSQL: true,
		span:    nil,
	})
	return
}

六、單元測試

單元測試代碼

// XORM技術文檔範例
type User struct {
	Id   int64
	Name string `xorm:"varchar(25) notnull unique 'usr_name' comment('姓名')"`
}

func TestNewEngine(t *testing.T) {
	// 初始化XORM引擎
	engine, err := NewEngine()
	if err != nil {
		t.Fatal(err)
	}

	// 初始化Tracer
	closer, err := initJaeger()
	if err != nil {
		t.Fatal(err)
	}
	defer closer.Close()

	// 生成新的Span - 注意將span結束掉,不然無法發送對應的結果
	span := opentracing.StartSpan("xorm sync")
	defer span.Finish()

	// 把生成的Root Span寫入到Context上下文,獲取一個子Context
	ctx := opentracing.ContextWithSpan(context.Background(), span)

	// 將子上下文傳入Session
	session := engine.Context(ctx)

	// Sync2同步表結構
	if err := session.Sync2(&User{}); err != nil {
		t.Fatal(err)
	}

	// 插入一條數據
	if _, err := session.InsertOne(&User{Name: "test"}); err != nil {
		t.Fatal()
	}
}

最終執行結果如下

2020-06-15T23:49:12.888+0800	INFO	zap/logger.go:38	[SQL] SELECT `TABLE_NAME`, `ENGINE`, `AUTO_INCREMENT`, `TABLE_COMMENT` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB') [test] - 7.0021ms
2020-06-15T23:49:13.633+0800	INFO	zap/logger.go:38	[SQL] CREATE TABLE IF NOT EXISTS `user` (`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT NOT NULL, `usr_name` VARCHAR(25) NOT NULL COMMENT '姓名') DEFAULT CHARSET utf8mb4 [] -   730.5132ms
2020-06-15T23:49:14.252+0800	INFO	zap/logger.go:38	[SQL] CREATE UNIQUE INDEX `UQE_user_usr_name` ON `user` (`usr_name`) [] - 619.0328ms
2020-06-15T23:49:14.367+0800	INFO	zap/logger.go:38	[SQL] INSERT INTO `user` (`usr_name`) VALUES (?) [test] - 115.1608ms

使用JaegerUI查看鏈路追蹤的結果,可以清晰的看到每一條SQL語句執行的順序、時間、內容、參數以及在整個Session所佔的比例,有效提高我們在企業項目中分析項目的短板

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章