链路跟踪-基本示例

单进程示例

单进程的链路跟踪即进程内方法之间的调用链关系。这种场景的跟踪没有涉及到分布式跟踪,比较简单,以该示例作为我们入门的一个例子吧。示例代码地址: https://github.com/gogf/gf/tree/master/example/trace/inprocess

Root Span

root span 即链路中第一个 span 对象。在这里的单进程场景中,往往需要手动创建一个。随后在方法内部创建的 span 都会作为它的子级 span。

链路跟踪-基本示例 - 图1 提示

在分布式架构的服务间通信场景中,往往不需要开发者手动创建 root span,而是由客户端/服务端请求的拦截器来自动创建。

创建 tracer,生成 root span:

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

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

    ctx, span := gtrace.NewSpan(ctx, "main")
    defer span.End()

    // Trace 1.
    user1 := GetUser(ctx, 1)
    g.Dump(user1)

    // Trace 2.
    user100 := GetUser(ctx, 100)
    g.Dump(user100)
}

上述代码创建了一个 root span,并将该 span 通过 context 传递给 GetUser 方法,以便在 GetUser 方法中将追踪链继续延续下去。

方法间Span创建

// GetUser retrieves and returns hard coded user data for demonstration.
func GetUser(ctx context.Context, id int) g.Map {
    ctx, span := gtrace.NewSpan(ctx, "GetUser")
    defer span.End()
    m := g.Map{}
    gutil.MapMerge(
        m,
        GetInfo(ctx, id),
        GetDetail(ctx, id),
        GetScores(ctx, id),
    )
    return m
}

// GetInfo retrieves and returns hard coded user info for demonstration.
func GetInfo(ctx context.Context, id int) g.Map {
    ctx, span := gtrace.NewSpan(ctx, "GetInfo")
    defer span.End()
    if id == 100 {
        return g.Map{
            "id":     100,
            "name":   "john",
            "gender": 1,
        }
    }
    return nil
}

// GetDetail retrieves and returns hard coded user detail for demonstration.
func GetDetail(ctx context.Context, id int) g.Map {
    ctx, span := gtrace.NewSpan(ctx, "GetDetail")
    defer span.End()
    if id == 100 {
        return g.Map{
            "site":  "https://goframe.org",
            "email": "john@goframe.org",
        }
    }
    return nil
}

// GetScores retrieves and returns hard coded user scores for demonstration.
func GetScores(ctx context.Context, id int) g.Map {
    ctx, span := gtrace.NewSpan(ctx, "GetScores")
    defer span.End()
    if id == 100 {
        return g.Map{
            "math":    100,
            "english": 60,
            "chinese": 50,
        }
    }
    return nil
}

该示例代码展示了多层级方法间的链路信息传递,即是把 ctx 上下文变量作为第一个方法参数传递即可。在方法内部,我们通过的固定语法来创建/开始一个 Span:

ctx, span := gtrace.NewSpan(ctx, "xxx")
defer span.End()

并通过 defer 的方式调用 span.End 来结束一个 Span,这样可以很好地记录 Span 生命周期(开始和结束)信息,这些信息都将会展示到链路跟踪系统中。其中 gtrace.NewSpan 方法的第二个参数 spanName 我们直接给定方法的名称即可,这样在链路展示中比较有识别性。

效果查看

执行完上面的程序后,终端输出:

链路跟踪-基本示例 - 图2

打开 Jaeger UI: http://localhost:16686/search,可以看到链路追踪的结果: 链路跟踪-基本示例 - 图3

链路跟踪-基本示例 - 图4

点击详情可以查看具体信息,包括 span 的调用顺序、调用关系,执行时间轴,以及记录一些Attributes和 Events 信息,极大的方便我们定位系统中的异常和发现性能瓶颈。:

链路跟踪-基本示例 - 图5

其中的 tracing-inprocess 是我们 tracer 的名称,该名称往往是服务名称,由于我们这里只有一个进程和一个 tracer,因此这里只看得到一个服务名称。其中的 main 为我们创建的 root span 名称,其他的 span 为基于该 root span 创建的子级 span。由于我们在程序中调用了两次 GetUser 方法,因此这里也展示了两次 GetUser 方法的调用。每一次 GetUser 调用的内部又分别去调用了 GetIndo、GetDetail、GetScores 三个方法,方法间的调用层级关系展示得非常清晰明了,并且每个方法的调用时长都可以看得到。

关于其中每个 span 记录的 Tags 和 Process 信息其实对应了 OpenTelemetry 中的 Attributes 和 Events 信息,这些信息我们放到后续章节去详细介绍。