入门
在本文中,我将展示你需要配置和处理统计信息所需的基本代码。在这个简短的教程中,我们将使用 Opentelemetry 来集成我们的 Golang 代码,并且为了可视化,我们将使用 Jeager。
在开始之前,让我简要介绍一下什么是 Opentelemetry。
什么是 Opentelemetry?
OpenTelemetry 是一个开源项目,旨在促进微服务和分布式应用的可观察性和可追踪性。它提供了一种标准化的方式来实现、生成记录并收集我们服务的指标,并且可以在不同的编程语言中实现。
其主要目标之一是解决以一致可靠的方式从分布式系统中收集遥测数据的挑战。Opentelemetry 是如何实现的呢?这项技术提供了库和工具,允许开发人员自动向他们的应用程序和组件添加工具,从而轻松生成和收集遥测数据,比如响应时间、延迟、错误、执行跟踪和性能指标等信息。
注意:
你可以在以下官方链接中找到更多有关 Opentelemetry 的信息:
https://opentelemetry.io/docs/what-is-opentelemetry/
我之前提到的另一个技术是 Jaeger。虽然它不是本文的中心话题,但我们将使用它来可视化收集的遥测数据。
Jaeger 是一个用于监视和调试分布式系统的开源跟踪平台。它为请求在分布式环境中传播时提供跟踪和追踪功能。
它的主要功能是捕获、存储和显示关于分布式系统中请求流动的详细信息。这包括有关运行时、依赖关系以及单个组件之间的交互的信息。
注意:
你可以在官方网站中找到关于 Jaeger 的更多信息:https://www.jaegertracing.io/docs/1.45/
实施时间!
现在你已经了解了 OpenTelemetry 和 Jeager,是时候构建两个简单的应用程序了。
- 第一个将向我们展示如何在我们的 Golang 应用程序中配置 OpenTelemetry,并通过运行一些函数来学习。在这里,我们将了解跟踪的工作原理,以及如何在 Jeager 上可视化它们。
- 第二个是基于 gin-gonic 框架的服务实现。
我们开始吧!
首先,我们需要运行一个 Jeager 实例,在这种情况下,我们将使用 Docker 使其变得简单。
Jaeger 提供了一个我们可以用于教程的 all-in-one 发行版。
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-e COLLECTOR_OTLP_ENABLED=true \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:1.45
注意:对于生产环境,你应该根据需要进行适当的配置。
运行并构建容器后,你可以通过以下链接访问 Jaeger:http://127.0.0.1:16686/search。
你将看到如下页面:
持续运行并让我们开始编码吧。
集成 Opentelemetry
让我们从一个简单的示例开始:
我们需要以下依赖:
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
你需要配置应用程序以将遥测数据发送到 Jaeger。此函数使用默认的 Jaeger URL:PORT 创建导出器。
我将尽量添加良好的注释,以指导你理解代码。
首先,定义下面几个常量,我们将在配置 OpenTelemetry 和导出器的函数中使用它们。
- Service:帮助我们识别生成 span 的服务。
- Environment:帮助我们按部署(例如生产、测试、开发)对服务 span 进行分组。
- ID:帮助我们标识 span 组。
const (
service = "medium-tutorial"
environment = "development"
id = 1
)
然后是函数:
func tracerProvider() (*tracesdk.TracerProvider, error) {
// 创建 Jaeger 导出器
exp, err := jaeger.New(jaeger.WithCollectorEndpoint())
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// 在生产环境中始终确保进行批处理。
tracesdk.WithBatcher(exp),
// 记录有关此应用程序的信息到资源中。
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(service),
attribute.String("environment", environment),
attribute.Int64("ID", id),
)),
)
return tp, nil
}
正如你所见,在此我们创建了导出器,同时我们使用了 Jeager(你可以在官方网站上查看完整的导出器列表)。之后,我们开始配置跟踪器提供程序,在那里我们定义了诸如 exporter
、serviceName
等参数,这些参数有助于在 Jaeger 上识别指标,并且像 environment
这样的参数有助于我们识别部署情况。
对于 main
方法,我们需要定义类似下面这样的代码示例:
tp, err := tracerProvider()
if err != nil {
log.Fatal(err)
}
// Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it.
otel.SetTracerProvider(tp)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Cleanly shutdown and flush telemetry when the application exits.
defer func(ctx context.Context) {
// Do not make the application hang when it is shutdown.
ctx, cancel = context.WithTimeout(ctx, time.Second*5)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}(ctx)
tr := tp.Tracer("component-main")
ctx, span := tr.Start(ctx, "foo")
defer span.End()
代码并不复杂,我们只是利用创建连接的函数,然后需要创建一个上下文。
我们的指标从这里开始:
tr := tp.Tracer("component-main")
ctx, span := tr.Start(ctx, "foo")
defer span.End()
这些代码行将帮助我们启动 span,跟踪器是主要组件,span 名称为 foo,并且你可以看到我们捕获了上下文,因为我们将使用该上下文来分组 span。(别担心,我会向你展示在 Jaeger 中的结果)
最后,这是完整的代码:
package main
import (
"context"
"fmt"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
const (
service = "medium-tutorial"
environment = "development"
id = 1
)
// tracerProvider returns an OpenTelemetry TracerProvider configured to use
// the Jaeger exporter that will send spans to the provided url. The returned
// TracerProvider will also use a Resource configured with all the information
// about the application.
func tracerProvider() (*tracesdk.TracerProvider, error) {
// Create the Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint())
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// Always be sure to batch in production.
tracesdk.WithBatcher(exp),
// Record information about this application in a Resource.
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(service),
attribute.String("environment", environment),
attribute.Int64("ID", id),
)),
)
return tp, nil
}
func main() {
tp, err := tracerProvider()
if err != nil {
log.Fatal(err)
}
// Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it.
otel.SetTracerProvider(tp)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Cleanly shutdown and flush telemetry when the application exits.
defer func(ctx context.Context) {
// Do not make the application hang when it is shutdown.
ctx, cancel = context.WithTimeout(ctx, time.Second*5)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}(ctx)
tr := tp.Tracer("component-main")
ctx, span := tr.Start(ctx, "foo")
defer span.End()
bar(ctx)
time.Sleep(10 * time.Second)
}
func bar(ctx context.Context) {
fmt.Println("on bar")
tr := otel.Tracer("component-bar")
_, span := tr.Start(ctx, "bar")
span.SetAttributes(attribute.Key("medium_test").String("this is an attribute value"))
defer span.End()
time.Sleep(200 * time.Millisecond)
}
如果我们运行程序,然后转到 Jaeger 的主页面,可以通过选择以下参数来搜索我们的统计信息:
服务:medium-tutorial
操作:所有
在上面的图像中,您可以看到当前的跨度。如果单击其中一个,将会看到更多信息:
在那里,您可以看到上面的注释行,您可以可视化跨度及其子跨度,这非常有帮助,因为可以追踪您的逻辑传播和流程。您可以在那里可视化的其他重要指标是每个过程(span)完成所花费的时间。
这里我想留下一条注释:如果您想了解更多有关跨度属性的信息,建议您查阅官方文档,本文只是一个简单的教程。
很简单,对吧?现在我们知道如何发送跨度以及如何对它们进行分组,我们可以将其实现到我们的 gin-gonic 实现中。让我们看一下它是什么样子的。
在 gin-gonic 服务中实现 OpenTelemetry
在以下代码中,您将找到一个简单的 gin-gonic 的 open telemetry 实现,我们可以重用用于配置导出器的基本配置。
此外,在此代码中,您将了解如何处理 Go 协程跨度。
以下是完整的代码:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
oteltrace "go.opentelemetry.io/otel/trace"
)
const (
service = "medium-gin-server-test"
environment = "development"
id = 1
)
var tracer = otel.Tracer(service)
func tracerProvider() (*tracesdk.TracerProvider, error) {
// Create the Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint())
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// Always be sure to batch in production.
tracesdk.WithSampler(tracesdk.AlwaysSample()),
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(service),
attribute.String("environment", environment),
attribute.Int64("ID", id),
),
),
)
// Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it.
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return tp, nil
}
func main() {
fmt.Println("initializing")
tp, err := tracerProvider()
if err != nil {
log.Fatal(err)
}
// Cleanly shutdown and flush telemetry when the application exits.
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
}()
r := gin.New()
r.Use(otelgin.Middleware("my-server"))
loadRoutes(r)
r.Run()
}
func loadRoutes(r *gin.Engine) {
r.GET("/ping", pingFunc)
}
func pingFunc(c *gin.Context) {
ctx, span := tracer.Start(c.Request.Context(), "/ping", oteltrace.WithAttributes(attribute.String("hello", "the user")))
defer span.End()
bar(ctx)
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
}
func ping2Func(c *gin.Context) {
ctx, span := tracer.Start(c.Request.Context(), "/ping-2", oteltrace.WithAttributes(attribute.String("hello2", "the user 2")))
defer span.End()
bar(ctx)
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
}
func bar(ctx context.Context) {
fmt.Println("on bar")
// Use the global TracerProvider.
ct, span := tracer.Start(ctx, "bar")
span.SetAttributes(attribute.Key("testset").String("value"))
defer span.End()
time.Sleep(1 * time.Millisecond)
go bar3(ct)
}
func bar3(ctx context.Context) {
fmt.Println("on bar 3")
_, span := tracer.Start(ctx, "bar-3-on-goroutine")
span.AddEvent("starting goroutine bar3")
defer func() {
span.End()
}()
span.AddEvent("executing logic")
time.Sleep(1 * time.Second)
span.AddEvent("completed goroutine bar3")
}
运行代码并向 ping
端点发出请求后,您可以转到 Jaeger 并搜索跨度。
这将显示类似于以下的内容:
然后,如果单击结果,您将能够查看所有跨度的追踪:
在此示例中,我们可以将跨度视为级联,因为当我们通过函数调用传递上下文时,我们生成了跨度层次结构。
现在您可以使用代码并进行修改,以便更深入了解跨度追踪。
正如您所见,使用可视化跟踪和跨度的工具将 OpenTelemetry 集成到我们的项目中并不复杂。
您可以使用我们示例中定义的代码开始配置您的真实服务,只需根据需要进行调整即可。正如您所见,这并不复杂,易于理解。