Gin 通过中间件接入 Jaeger 收集 Tracing
前言
Distributed tracing, also called distributed request tracing, is a method used to profile and monitor applications. Distributed tracing helps pinpoint where failures occur and what causes poor performance.Distributed tracing is particularly well-suited to debugging and monitoring modern distributed software architectures, such as microservices.
分布式追踪,也被成为分布式请求跟踪,是一种分析和监控应用程序的手段。分布式追踪有助于查明故障发生的位置以及导致性能低下的原因。此外,分布式跟踪十分合适调试和监控现代分布式架构的软件,尤其是那些使用微服务体系结构构建的应用程序
如上述引用所属,分布式追踪在现代分布式架构的软件中起着举足轻重的作用,尤其是在微服务这类调用链冗长且复杂的软件架构中,分布式追踪的出现使得上述分析和监控应用程序等工作变得更加简单且方便。
OpenTracing
OpenTracing API提供了一个标准的、与供应商无关的框架,这意味着如果开发者想要尝试一种不同的分布式追踪系统,开发者只需要简单地修改Tracer配置即可,而不需要替换整个分布式追踪系统。
OpenTracing 由 API 规范(描述了语言无关的数据模型和 Opentracing API 指南)、实现该规范的框架和库以及项目文档组成,OpenTracing 不是一个标准,OpenTracing API 项目正致力于为分布式跟踪创建更加标准化的 API 和工具。
Jaeger
Jaeger 是由 Uber 开发的且兼容 OpenTracing API 的分布式追踪系统,用于监控和调试基于微服务体系结构构建的应用程序,主要功能包括:
- 分布式上下文传播
- 分布式事务监控
- 根因分析
- 服务依赖分析
- 性能和延迟优化
测试环境
测试环境使用 Docker 进行测试,测试镜像为 Uber 官方提供的 All-In-One
镜像:
version: '3'
services:
jaeger:
container_name: local-jaeger
image: jaegertracing/all-in-one:1.18
ports:
- "5778:5778"
- "6831-6832:6831-6832/udp"
- "14250:14250"
- "14268:14268"
- "16686:16686"
networks:
- local
networks:
local:
初始化 Jaeger 并注册为全局 Tracer
func NewGlobalTestTracer() (opentracing.Tracer, io.Closer, error) {
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
},
}
// 示例 Logger 和 Metric 分别使用 github.com/uber/jaeger-client-go/log 和 github.com/uber/jaeger-lib/metrics
jLogger := jaegerlog.StdLogger
jMetricsFactory := metrics.NullFactory
// 初始化 Tracer 实例
tracer, closer, err := cfg.NewTracer(
"serviceName",
jaegercfg.Logger(jLogger),
jaegercfg.Metrics(jMetricsFactory),
// 设置最大 Tag 长度,根据情况设置
jaegercfg.MaxTagValueLength(65535),
)
if err != nil {
log.Printf("Could not initialize jaeger tracer: %s", err.Error())
panic(err)
}
return tracer, closer, err
}
编写 Gin 中间件
func OpenTracing() gin.HandlerFunc {
return func(c *gin.Context) {
// 使用 opentracing.GlobalTracer() 获取全局 Tracer
wireCtx, _ := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(c.Request.Header),
)
// OpenTracing Span 概念,详情参见 https://opentracing.io/docs/overview/spans/
serverSpan := opentracing.StartSpan(
c.Request.URL.Path,
ext.RPCServerOption(wireCtx),
)
defer serverSpan.Finish()
// 记录请求 Url
ext.HTTPUrl.Set(serverSpan, c.Request.URL.Path)
// Http Method
ext.HTTPMethod.Set(serverSpan, c.Request.Method)
// 记录组件名称
ext.Component.Set(serverSpan, "Gin-Http")
// 自定义 Tag X-Forwarded-For
opentracing.Tag{Key: "http.headers.x-forwarded-for", Value: c.Request.Header.Get("X-Forwarded-For")}.Set(serverSpan)
// 自定义 Tag User-Agent
opentracing.Tag{Key: "http.headers.user-agent", Value: c.Request.Header.Get("User-Agent")}.Set(serverSpan)
// 自定义 Tag Request-Time
opentracing.Tag{Key: "request.time", Value: time.Now().Format(time.RFC3339)}.Set(serverSpan)
// 自定义 Tag Server-Mode
opentracing.Tag{Key: "http.server.mode", Value: gin.Mode()}.Set(serverSpan)
body, err := ioutil.ReadAll(c.Request.Body)
if err == nil {
// 自定义 Tag Request-Body
opentracing.Tag{Key: "http.request_body", Value: string(body)}.Set(serverSpan)
}
c.Request = c.Request.WithContext(opentracing.ContextWithSpan(c.Request.Context(), serverSpan))
c.Next()
if gin.Mode() == gin.DebugMode {
// 自定义 Tag StackTrace
opentracing.Tag{Key: "debug.trace", Value: string(debug.Stack())}.Set(serverSpan)
}
ext.HTTPStatusCode.Set(serverSpan, uint16(c.Writer.Status()))
opentracing.Tag{Key: "request.errors", Value: c.Errors.String()}.Set(serverSpan)
}
}
启动 Gin 服务
package main
import (
"io/ioutil"
"runtime/debug"
"time"
"github.com/gin-gonic/gin"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/gin-gonic/gin"
)
func main() {
t, closer, err := NewGlobalTestTracer()
if err != nil {
panic(err)
}
defer closer.Close()
opentracing.SetGlobalTracer(t)
r := gin.Default()
// 注入先前编写的中间件
r.Use(OpenTracing())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
}
结果验证
打开 Jaeger UI 进行验证,按照对应的测试环境,Jaeger Web UI 地址为 http://127.0.0.1:16686/search
Tags:
component=Gin-Http
debug.trace=goroutine 60 [running]: runtime/debug.Stack(0xc00017c0f0, 0xc00062c2d0, 0x54a2380) /usr/local/Cellar/go/1.15/libexec/src/runtime/debug/stack.go:24 +0x9f fusion-pc.com/eam/internal/pkg/plugins/jaeger.OpenTracing.func1(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/internal/pkg/plugins/jaeger/jaeger_gin.go:43 +0x81c github.com/gin-gonic/gin.(*Context).Next(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/context.go:161 +0x3b github.com/gin-contrib/zap.RecoveryWithZap.func1(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-contrib/zap/zap.go:109 +0x68 github.com/gin-gonic/gin.(*Context).Next(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/context.go:161 +0x3b github.com/gin-contrib/zap.Ginzap.func1(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-contrib/zap/zap.go:32 +0xce github.com/gin-gonic/gin.(*Context).Next(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/context.go:161 +0x3b github.com/gin-gonic/gin.RecoveryWithWriter.func1(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/recovery.go:83 +0x65 github.com/gin-gonic/gin.(*Context).Next(0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/context.go:161 +0x3b github.com/gin-gonic/gin.(*Engine).handleHTTPRequest(0xc00053a000, 0xc00017c0f0) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/gin.go:409 +0x67a github.com/gin-gonic/gin.(*Engine).ServeHTTP(0xc00053a000, 0x5487700, 0xc0006281c0, 0xc000372600) /Users/zhouhaowei/Documents/Project/eam/vendor/github.com/gin-gonic/gin/gin.go:367 +0x14d net/http.serverHandler.ServeHTTP(0xc0006280e0, 0x5487700, 0xc0006281c0, 0xc000372600) /usr/local/Cellar/go/1.15/libexec/src/net/http/server.go:2843 +0xa3 net/http.(*conn).serve(0xc0000e0140, 0x548c680, 0xc0005643c0) /usr/local/Cellar/go/1.15/libexec/src/net/http/server.go:1925 +0x8ad created by net/http.(*Server).Serve /usr/local/Cellar/go/1.15/libexec/src/net/http/server.go:2969 +0x36c
http.headers.user-agent=PostmanRuntime/7.26.3
http.headers.x-forwarded-for=
http.method=GET
http.request_body=
http.server.mode=debug
http.status_code=200
http.url=/ping
internal.span.format=proto
request.errors=
request.time=2020-09-01T20:55:34+08:00
sampler.param=1
sampler.type=probabilistic
span.kind=server
Process:
client-uuid=8060d5ba910b166
hostname=zhouhaoweis-MacBook-Pro.localip=192.168.31.83
jaeger.version=Go-2.25.0
延伸: 从配置初始化 Jaeger
package jaeger
import (
"fmt"
"strings"
"time"
"github.com/google/wire"
"github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
// 使用 zap 作为日志输出
jaegerZap "github.com/uber/jaeger-client-go/log/zap"
"go.uber.org/zap"
)
type Sampler struct {
SamplerType string
Probability float64
Rate int64
}
type Reporter struct {
QueueSize int
LogSpans bool
BufferFlushInterval time.Duration
Username string
Password string
}
type Options struct {
Enable bool
ServiceName string
Host string
Port uint16
Sampler *Sampler
Reporter *Reporter
}
const defaultSamplingLimitRate = 1
var NoDefaultTracingConfigError = errors.New("unable to load tracing config")
func NewOptions(v *viper.Viper, logger *zap.Logger) (opts *Options, err error) {
cfg := v.GetString("tracing.default")
if len(cfg) <= 0 {
return nil, NoDefaultTracingConfigError
}
opts = new(Options)
key := fmt.Sprintf("tracing.%s", cfg)
if err = v.UnmarshalKey(key, opts); err != nil {
return nil, errors.Wrap(err, "unmarshal tracing options error")
}
logger.With(zap.String("type", "tracing")).Info(fmt.Sprintf("load %s tracing options success", cfg))
return
}
func NewTracer(logger *zap.Logger, opts *Options) (opentracing.Tracer, error) {
sampleCfg := &config.SamplerConfig{}
switch strings.ToLower(opts.Sampler.SamplerType) {
case jaeger.SamplerTypeConst:
sampleCfg.Type = jaeger.SamplerTypeConst
case jaeger.SamplerTypeRateLimiting:
sampleCfg.Type = jaeger.SamplerTypeRateLimiting
if opts.Sampler.Rate > 0 {
sampleCfg.Param = float64(opts.Sampler.Rate)
} else {
sampleCfg.Param = defaultSamplingLimitRate
}
case jaeger.SamplerTypeRemote:
sampleCfg.Type = jaeger.SamplerTypeRemote
default:
sampleCfg.Type = jaeger.SamplerTypeProbabilistic
if opts.Sampler.Probability > 0 {
sampleCfg.Param = opts.Sampler.Probability
}
}
reporterCfg := &config.ReporterConfig{
QueueSize: opts.Reporter.QueueSize,
LogSpans: opts.Reporter.LogSpans,
BufferFlushInterval: opts.Reporter.BufferFlushInterval,
}
if len(opts.Reporter.Username) > 0 && len(opts.Reporter.Password) > 0 {
reporterCfg.User = opts.Reporter.Username
reporterCfg.Password = opts.Reporter.Password
}
cfg := config.Configuration{
ServiceName: opts.ServiceName,
Sampler: sampleCfg,
Reporter: reporterCfg,
}
addr := fmt.Sprintf("%s:%d", opts.Host, opts.Port)
sender, err := jaeger.NewUDPTransport(addr, 0)
if err != nil {
return nil, err
}
reporter := jaeger.NewRemoteReporter(sender)
tracer, _, err := cfg.NewTracer(
config.Reporter(reporter),
config.Logger(jaegerZap.NewLogger(logger.With(zap.String("type", "tracing")))),
config.MaxTagValueLength(65535),
)
opentracing.SetGlobalTracer(tracer)
return tracer, err
}
var ProviderSet = wire.NewSet(NewOptions, NewTracer)