安装 jaeger
jaeger 提供一个 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 9411:9411
jaegertracing/all-in-one:1.6
OpenTracing
OpenTracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。 OpenTracing 提供了用于运营支撑系统的和针对特定平台的辅助程序库。
jaeger 兼容 OpenTracing API,所以我们使用 OpenTracing 的程序库可以方便的替换追踪工具。
OpenTracing 中文文档
jaeger 使用
封住一下 jaeger 的初始化操作方便使用,详细用法可以查看 jaeger-client-go
// lib/tracer
// NewTracer 创建一个jaeger Tracer
func NewTracer(servicename string, addr string) (opentracing.Tracer, io.Closer, error) {
cfg := jaegercfg.Configuration{
ServiceName: servicename,
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
},
}
sender, err := jaeger.NewUDPTransport(addr, 0)
if err != nil {
return nil, nil, err
}
reporter := jaeger.NewRemoteReporter(sender)
// Initialize tracer with a logger and a metrics factory
tracer, closer, err := cfg.NewTracer(
jaegercfg.Reporter(reporter),
)
return tracer, closer, err
}
func main() {
t, io, err := tracer.NewTracer("tracer", "")
if err != nil {
log.Fatal(err)
}
defer io.Close()
opentracing.SetGlobalTracer(t)
}
opentracing.SetGlobalTracer(t) 方法执行会将 jaeger tracer 注册到全局,接下来只需要使用 opentracing 的标准 API 便可以了。
如果不想使用 jaeger 了,想替换成其他分布式追踪工具,只需要工具支持 opentracing 标准,并将 main 函数的 SetGlobalTracer 操作替换即可,其他文件都不需要更改。
micro 链路追踪插件
micro 自带的 opentracing 插件
在 micro 自带的插件中已经有 opentracing 的插件了,包含 server,client 等,不过这个插件只能 go-micro 构建的微服务(api,srv)中使用。因为 micro 网关有一个独立的插件系统,但是并没有提供 opentracing 相关的插件。
micro/go-plugins/wrapper/trace/opentracing/opentracing.go
我们可以在构建服务的时候直接使用,只需要在服务初始化时增加一行函数就可以了。
service := micro.NewService(
micro.Name(name),
micro.Version("latest"),
micro.WrapHandler(ocplugin.NewHandlerWrapper(opentracing.GlobalTracer())),
)
srv/user/main.go 目录下的 user 服务是一个完整的使用实例。
为 micro 网关增加 opentracing 插件
实现原理
外部 HTTP 请求首先经过 API 网关,网关生成第一个 SpanContexts 并且通过 HTTP 头传递到聚合层的 API 服务,这边需要我们实现一个插件去做这件事,原理很简单,拦截每一次请求添加信息就可以了。
查看 micro 自带的 opentracing 插件,可以发现是通过 golang 的 context 传递,micro 的 RPC 已经封装好了通过 context 在跨进程服务间传递 SpanContexts 机制,所以我们需要在 API 服务层实现一个插件,从 HTTP 头中取出 SpanContexts 并按照 micro 自带的方式注入 golang context。
// micro opentracing插件中wHandlerWrappe
// NewHandlerWrapper accepts an opentracing Tracer and returns a Handler Wrapper
func NewHandlerWrapper(ot opentracing.Tracer) server.HandlerWrapper {
return func(h server.HandlerFunc) server.HandlerFunc {
return func(ctx context.Context, req server.Request, rsp interface{}) error {
name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
ctx, span, err := traceIntoContext(ctx, ot, name)
if err != nil {
return err
}
defer span.Finish()
return h(ctx, req, rsp)
}
}
}
micro API 网关插件
lib/wrapper/tracer/opentracing/stdhttp/stdhttp.go
和实现 JWT 鉴权插件一样,实现一个 HTTP 中间件通过 mciro 的插件机制全局注册就可以实现拦截每次请求并处理。
// TracerWrapper tracer wrapper
func TracerWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
spanCtx, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
sp := opentracing.GlobalTracer().StartSpan(r.URL.Path, opentracing.ChildOf(spanCtx))
defer sp.Finish()
if err := opentracing.GlobalTracer().Inject(
sp.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(r.Header)); err != nil {
log.Println(err)
}
sct := &status_code.StatusCodeTracker{ResponseWriter: w, Status: http.StatusOK}
h.ServeHTTP(sct.WrappedResponseWriter(), r)
ext.HTTPMethod.Set(sp, r.Method)
ext.HTTPUrl.Set(sp, r.URL.EscapedPath())
ext.HTTPStatusCode.Set(sp, uint16(sct.Status))
if sct.Status >= http.StatusInternalServerError {
ext.Error.Set(sp, true)
}
})
}
Tracer 相关的概念可以查看这个文档
- opentracing.GlobalTracer().Extract 方法提取 HTTP 头中的 spanContexts
- opentracing.ChildOf 方法基于提取出来的 spanContexts 生成新的 child spanContexts
- opentracing.GlobalTracer().StartSpan 方法生成一个新的 span
- github.com/opentracing/opentracing-go/ext 通过 ext 可以为追踪添加一些 tag 来展示更多信息,比如 URL,请求类型(GET,POST...), 返回码
- sp.Finish() 结束这一个 span
API 服务(使用 gin)插件
lib/wrapper/tracer/opentracing/gin2micro/gin2micro.go
// TracerWrapper tracer 中间件
func TracerWrapper(c *gin.Context) {
md := make(map[string]string)
spanCtx, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header))
sp := opentracing.GlobalTracer().StartSpan(c.Request.URL.Path, opentracing.ChildOf(spanCtx))
defer sp.Finish()
if err := opentracing.GlobalTracer().Inject(sp.Context(),
opentracing.TextMap,
opentracing.TextMapCarrier(md)); err != nil {
log.Log(err)
}
ctx := context.TODO()
ctx = opentracing.ContextWithSpan(ctx, sp)
ctx = metadata.NewContext(ctx, md)
c.Set(contextTracerKey, ctx)
c.Next()
statusCode := c.Writer.Status()
ext.HTTPStatusCode.Set(sp, uint16(statusCode))
ext.HTTPMethod.Set(sp, c.Request.Method)
ext.HTTPUrl.Set(sp, c.Request.URL.EscapedPath())
if statusCode >= http.StatusInternalServerError {
ext.Error.Set(sp, true)
}
}
// ContextWithSpan 返回context
func ContextWithSpan(c *gin.Context) (ctx context.Context, ok bool) {
v, exist := c.Get(contextTracerKey)
if exist == false {
ok = false
return
}
ctx, ok = v.(context.Context)
return
}
基本操作流程和给 micro 编写的插件相同,但是有两点不同。其一,因为我使用 gin 开发 API 服务,所以基于 gin 的 API。其二,因为 micro 内部提供通过 golang context 传递 spanContexts 的机制,所以将这边会将 child spanContexts 注入到 gin 的 context,在 API 服务通过 micro 提供 RPC 接口(生成的 XX.micro.go 文件中调用函数第一个参数都是 context)调用其他服务时传入提取的 context,如下:
...
ctx, ok := gin2micro.ContextWithSpan(c)
if ok == false {
log.Log("get context err")
}
res, err := s.helloC.Call(ctx, &helloS.Request{Name: "xuxu"})
...
完整的实现细节可以查看,github 仓库中 lib/wrapper/tracer/opentracing, 这里。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于