云原生之开源遥测框架OpenTelemetry(在 Gin 框架中使用 OpenTelemetry 进行分布式追踪和监控)

发布于:2025-03-23 ⋅ 阅读:(27) ⋅ 点赞:(0)

云原生之开源遥测框架OpenTelemetry

背景

在云原生时代,随着系统应用的不断复杂化和分布式,传统的监控手段已经难以满足需求。因此,OpenTelemetry这一开源遥测框架应运而生,它旨在提供一套统一的解决方案,帮助开发者和运维团队全面了解系统性能状况,迅速定位和解决问题。

什么是可观测性?

可观测性是通过检查系统输出来理解系统内部状态的能力。 在软件的背景下,这意味着能够通过检查遥测数据(包括链路、指标和日志)来理解系统的内部状态。

要使系统可观测,必须对其进行仪表化。也就是说,代码必须发出链路、指标或日志。 然后,仪表化的数据必须发送到可观测性后端。

什么是 OpenTelemetry?

官方文档:https://opentelemetry.io/zh/docs/what-is-opentelemetry/

OpenTelemetry 是一个可观测性框架和工具包, 旨在创建和管理遥测数据,如链路、 指标和日志。 重要的是,OpenTelemetry 是供应商和工具无关的,这意味着它可以与各种可观测性后端一起使用, 包括 Jaeger 和 Prometheus 这类开源工具以及商业化产品。

OpenTelemetry 是云原生计算基金会 (CNCF)的一个项目,是由 OpenTracing 和 OpenCensus 项目合并而成的。原来这两个项目都是为解决同样的问题而创建的: 缺乏一种标准的方法来为代码进行仪表化并将遥测数据发送到可观测性后端。 由于这两个项目都无法独立解决这个问题,所以将其合并成立了 OpenTelemetry, 吸收了双方的优势,提供了统一的解决方案。

OpenTelemetry 不是像 Jaeger、Prometheus 或其他商业供应商那样的可观测性后端。 OpenTelemetry 专注于遥测数据的生成、采集、管理和导出。 OpenTelemetry 的一个主要目标是, 无论应用程序或系统采用何种编程语言、基础设施或运行时环境,你都可以轻松地将其仪表化。 重要的是,遥测数据的存储和可视化是有意留给其他工具处理的。

Opentelemetry的主要优势有以下几点:

支持开源通用标准:Opentelemetry使用开放式协议,使得开发人员可以更加灵活地根据需求选择适合的实现方法,如Jaeger、Zipkin等。这有助于确保数据的一致性和互通性。

跨语言支持:Opentelemetry支持众多编程语言,因此可以应用于任何一种混合语言的架构中。这使得多语言环境下的监控和观测变得更加容易,降低了跨语言集成的复杂性。

统一的SDK和自动化埋点方案:Opentelemetry提供了一套统一的SDK,简化了开发人员的工作。此外,它还支持自动化埋点方案,可以自动收集关键的性能指标,减少了手动配置的工作量。

数据采集和Traces/Metrics/Logs互通:Opentelemetry不仅关注跟踪数据,还关注度量和日志数据。这使得开发人员可以从多个维度全面了解系统性能状况,提高了问题的定位和解决效率。

理解分布式链路

分布式链路让你能够观察请求如何在复杂的分布式系统中传播。它提高了应用程序或系统健康状况的可见性,并让你能够调试那些难以在本地重现的行为。对于分布式系统来说,分布式链路是必不可少的,因为这些系统通常存在不确定性问题,或者过于复杂而无法在本地重现。

要理解分布式链路,你需要了解其各个组成部分的角色:日志、span(跨度)和 trace(链路)。

日志

日志 日志是由服务或其他组件发出的带时间戳的消息。与链路不同,它们不一定与特定的用户请求或事务相关联。在软件中几乎到处都能找到日志。长期以来,开发人员和运维人员一直依靠日志来洞察系统行为。

日志虽然有用,但仅靠它们来追踪代码执行还不够,因为日志通常缺乏上下文信息,比如它们是从哪里被调用的。

当日志作为 span(跨度)的一部分,或者与 trace(链路)和 span 关联起来时,它们的价值就会大大增加。

要深入了解日志以及它们与OpenTelemetry的关系,请参阅日志章节。

Spans

Spans
Span(跨度)是分布式链路中的基本构建块,它代表了一个具体的操作或工作单元。每个 span 都记录了请求中的特定动作,帮助我们了解操作执行过程中发生的详细情况。

一个 span 包含名称、时间相关的数据、结构化的日志消息,以及其他元数据(属性),这些信息共同描绘了该操作的完整画面。

在这里插入图片描述

分布式链路

分布式链路,通常简称为链路,记录了请求(无论是来自应用程序还是终端用户)在多服务架构(如微服务和无服务器应用)中传播的路径。

**一个链路由一个或多个 span 组成。第一个 span 被称为根 span,它代表了一个请求从开始到结束的全过程。**根 span 下的子 span 则提供了请求过程中更详细的上下文信息(或者说,构成了请求的各个步骤)。

如果没有链路,在分布式系统中找出性能问题的根源将会非常具有挑战性。链路通过分解请求在分布式系统中的流转过程,使得调试和理解分布式系统变得不那么令人生畏。

在这里插入图片描述
瀑布图清晰地展示了根 span 与其子 span 之间的父子关系。当一个 span 包含另一个 span 时,这种关系就表现为嵌套结构。

Trace:表示一个完整的请求路径,包含多个 Span
Span:表示请求中的一个操作(如 HTTP 请求、数据库查询)

在 Gin 框架中使用 OpenTelemetry 进行分布式追踪和监控

通过 OpenTelemetry,可以轻松实现 Gin 应用的分布式追踪和监控,提升系统的可观测性。

0. 整体思路

  1. 初始化 OpenTelemetry 的 TracerProvider 和 Exporter。
  2. 使用 otelgin.Middleware 自动追踪 Gin 请求。
  3. 可选:手动创建 Span 以增强追踪信息。
  4. 将数据导出到 Jaeger 或其他后端系统

运行和测试

  1. 启动 Jaeger。
  2. 编写和运行 Go 程序。
  3. 访问 Jaeger UI(http://localhost:16686),查看追踪数据。

1. 初始化 OpenTelemetry

在 Gin 应用启动时,初始化 OpenTelemetry 的 TracerProvider 和 Exporter。

package main

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

func initTracer() (*sdktrace.TracerProvider, error) {
	// 创建 OTLP HTTP Exporter
	ctx := context.Background()
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithInsecure(),
		otlptracehttp.WithEndpoint("localhost:4318"), // OpenTelemetry Collector 的地址
	)
	if err != nil {
		return nil, err
	}

	// 设置 TracerProvider
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("gin-server"), // 服务名称
		)),
	)
	otel.SetTracerProvider(tp)

	// 设置上下文传播
	otel.SetTextMapPropagator(propagation.TraceContext{})

	return tp, nil
}

关键字:otlptracehttp.New

  • TracerProvider 负责配置 Tracer 的行为,例如设置采样策略、资源信息(如服务名称)等。TracerProvider创建和管理 Tracer 实例。
  • Exporter 是 OpenTelemetry 中用于将遥测数据(如 Span、Metric)导出到外部系统的组件。它负责将数据发送到后端系统(如 Jaeger、Prometheus、OpenTelemetry Collector 等)。Exporter将 Span 或 Metric 数据导出到指定的后端系统。
工作流程
  1. TracerProvider 创建 Tracer。
  2. Tracer 创建 Span。
  3. Span 完成后,TracerProvider 将 Span 数据传递给注册的 SpanProcessor。
  4. SpanProcessor 将 Span 数据传递给 Exporter。
  5. Exporter 将 Span 数据导出到外部系统(如 Jaeger)。

SpanProcessor 是连接 TracerProvider 和 Exporter 的桥梁。它负责将 Span 数据从 TracerProvider 传递到 Exporter。

  • TracerProvider 是追踪数据的生产者,负责生成和管理 Span
  • Exporter 是追踪数据的消费者,负责将数据发送到外部系统
  • 两者通过 SpanProcessor 连接,共同实现分布式追踪和监控。

2. 集成 Gin 和 OpenTelemetry

使用 otelgin 中间件自动追踪 Gin 的请求。

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

func main() {
	// 初始化 OpenTelemetry
	tp, err := initTracer()
	if err != nil {
		log.Fatalf("Failed to initialize OpenTelemetry: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	// 创建 Gin 应用
	r := gin.Default()

	// 添加 OpenTelemetry 中间件
	r.Use(otelgin.Middleware("gin-server"))

	// 定义路由
	r.GET("/hello", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, OpenTelemetry!",
		})
	})

	// 启动服务器
	if err := r.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

关键字:otelgin.Middleware

在 Gin 应用中,通过 r.Use(otelgin.Middleware("service-name")) 注册 otelgin 中间件。

TracerProvider 是生成 Tracer 的工厂,但 Tracer 实例 由中间件在需要时通过 TracerProvider.Tracer() 自动创建

otelgin 中间件 是 OpenTelemetry 提供的一个专门用于 Gin 框架 的 Instrumentation 工具。它的主要能力是自动追踪 Gin 应用的 HTTP 请求,并生成相应的 Span 数据。

otelgin.WithTracerProvider(provider) // 将 TracerProvider 注入中间件 即 gin注册 Provider

Tracer 由 TracerProvider 在需要时动态创建,中间件会自动处理这些细节。

中间件行为:otelgin.Middleware 会自动使用 TracerProvider 创建 Tracer.

otelgin 中间件的能力

中间件行为:otelgin.Middleware 会自动使用 TracerProvider 创建 Tracer,并在请求处理过程中通过 Span 上下文传递追踪信息

  1. 自动追踪 HTTP 请求
    功能:otelgin 中间件会自动为每个进入 Gin 应用的 HTTP 请求创建 Span
    追踪内容:
    请求的路径(如 /hello)。
    HTTP 方法(如 GET、POST)。
    请求的状态码(如 200、404)。
    请求的耗时(从开始到结束的时间)。
    示例:
    对于 GET /hello 请求,otelgin 会生成一个 Span,记录请求的详细信息。

  2. 上下文传播
    功能:otelgin 中间件会自动处理分布式追踪中的上下文传播。
    场景:
    如果请求中包含了 Trace ID 和 Span ID(例如通过 HTTP 头 traceparent 传递),otelgin 会提取这些信息并继续追踪
    如果请求中没有上下文信息,otelgin 会创建一个新的 Trace ID 和 Span ID。
    作用:确保在分布式系统中,请求的追踪信息能够跨服务传递。

  3. 错误记录
    功能:otelgin 中间件会自动记录请求中的错误信息。
    场景:
    如果请求返回了错误状态码(如 500),otelgin 会将错误信息附加到 Span 中。
    如果请求中发生了 panic,otelgin 会捕获 panic 并记录到 Span 中。

  4. 与 OpenTelemetry 集成
    功能:otelgin 中间件与 OpenTelemetry 的全局 TracerProvider 集成,使用其配置的 Tracer 和 Exporter。
    作用:确保生成的 Span 数据能够被 OpenTelemetry 的 Exporter 导出到后端系统(如 Jaeger、Prometheus)。

otelgin中间件在请求开始时调用TracerProvider的Tracer方法获取Tracer实例。

关于otel-go-contrib-tracer

gin注册otelgin.Middleware中间件时,会自动给 gin context的 key 为otel-go-contrib-tracer存储一个tracker实例,

value := ctx.Value("otel-go-contrib-tracer")
tracer, ok := value.(trace.Tracer)

如上,这个就可以直接拿到 tracker实例,然后可以调spanctx, span = tracer.Start(c.Request.Context(), spanName)返回 一个包含span的context,以及span

3. 手动创建 Span

如果需要手动创建 Span,可以通过 otel.Tracer 实现。

r.GET("/manual", func(c *gin.Context) {
	// 获取 Tracer
	tracer := otel.Tracer("gin-server")

	// 创建 Span
	ctx, span := tracer.Start(c.Request.Context(), "manual-span")
	defer span.End()

	// 模拟一些操作
	time.Sleep(100 * time.Millisecond)

	c.JSON(http.StatusOK, gin.H{
		"message": "Manual Span Created!",
	})
})

总结:
通过ProviderOption注入的TracerProvider会在请求处理时自动创建Tracer实例并存入Context。

4. 导出数据到

japer 官方文档:https://www.jaegertracing.io/docs/1.66/apis/
jaeger的本地搭建和快速可以参考本人文章Jaeger安装和简单使用

可以选择将链路追踪的数据发送至 Jaeger,通过 Jaeger UI 查看

使用 OTLP(OpenTelemetry Protocol) 导出器将追踪数据发送到 Jaeger。

自 v1.35 以来,Jaeger 后端可以通过原生的 OpenTelemetry 协议(OTLP)接收来自 OpenTelemetry SDK 的跟踪数据。现在不再需要为 OpenTelemetry SDK 配置 Jaeger 导出程序,也不需要在 OpenTelemetry SDK 和 Jaeger 后端之间部署 OpenTelemetry 收集器。

如果你希望使用 OTLP(OpenTelemetry Protocol) 导出器将追踪数据发送到 Jaeger 或其他支持 OTLP 的后端(如 OpenTelemetry Collector),可以使用 go.opentelemetry.io/otel/exporters/otlp/otlptrace 包。OTLP 是 OpenTelemetry 的通用协议,支持通过 HTTP 或 gRPC 传输数据。

在这里插入图片描述
以下是使用 OTLP 导出器(支持 gRPC 和 HTTP)的示例代码。
将 OpenTelemetry 的追踪功能集成到 Gin 框架中,并使用 OTLP HTTP 导出器将追踪数据发送到 Jaeger 或 OpenTelemetry Collector:

  1. traceotel 包实现
package traceotel

import (
	"context"
	"log"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

// Register 创建一个 Gin 中间件,用于集成 OpenTelemetry 追踪功能
func Register(appname string, endpoint string) gin.HandlerFunc {
	opts := []otelgin.Option{
		ProviderOption(appname, endpoint),
		PropagationExtractOption(),
	}

	return otelgin.Middleware(appname, opts...)
}

// ProviderOption 初始化并返回一个 TracerProvider
func ProviderOption(appname string, endpoint string) otelgin.Option {
	return otelgin.WithTracerProvider(func() (*sdktrace.TracerProvider, error) {
		// 创建 OTLP HTTP Exporter
		ctx := context.Background()
		exporter, err := otlptracehttp.New(ctx,
			otlptracehttp.WithInsecure(), // 使用非安全连接(无 TLS)
			otlptracehttp.WithEndpoint(endpoint), // 追踪数据收集端点
		)
		if err != nil {
			return nil, err
		}

		// 设置 TracerProvider
		tp := sdktrace.NewTracerProvider(
			sdktrace.WithBatcher(exporter), // 使用批处理导出器
			sdktrace.WithResource(resource.NewWithAttributes(
				semconv.SchemaURL,
				semconv.ServiceName(appname), // 服务名称
			)),
		)

		return tp, nil
	})
}

// PropagationExtractOption 配置上下文传播器
func PropagationExtractOption() otelgin.Option {
	return otelgin.WithPropagators(propagation.TraceContext{})
}
  1. 在 Gin 应用中使用 traceotel 包,并启动一个简单的 HTTP 服务器。
package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"your-project/traceotel" // 替换为实际的包路径
)

func main() {
	// 创建 Gin 应用
	r := gin.Default()

	// 注册追踪中间件
	r.Use(traceotel.Register("gin-server", "localhost:4318"))

	// 定义路由
	r.GET("/hello", func(c *gin.Context) {
		// 模拟业务逻辑
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello, OpenTelemetry!",
		})
	})

	// 启动服务器
	if err := r.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

  • Register 函数通过 otelgin.Middleware 将 OpenTelemetry 的追踪功能集成到 Gin 框架中。
  • ProviderOption 函数用于初始化 TracerProvider,并配置 OTLP 导出器。
  • PropagationExtractOption 函数用于配置上下文传播器。
  1. 为什么不需要指定路径?
    OTLP 是 OpenTelemetry 的通用协议,用于传输遥测数据(如 Trace、Metric、Log)。
    它已经定义了默认的路径:
    gRPC:默认路径是 /opentelemetry.proto.collector.trace.v1.TraceService/Export。
    HTTP:默认路径是 /v1/traces。
    客户端只需要指定 endpoint 的主机和端口(如 localhost:4318),路径由 OTLP 协议自动处理。
  • 使用 OTLP 导出器时,客户端只需要指定 endpoint 的主机和端口,无需包含路径。
  1. 为什么我使用 /api/v1/traces Jaeger可以收到
    你使用 /api/v1/traces 时,Jaeger 仍然可以收到数据,是因为 Jaeger 的 HTTP 接收器支持多种路径,包括 /api/v1/traces 和 /v1/traces。

Jaeger 的 HTTP 接收器默认支持以下路径:

  • /api/v1/traces:这是 Jaeger 的传统路径,用于接收追踪数据。
  • /v1/traces:这是 OpenTelemetry 的 OTLP 协议默认路径。
    当 Jaeger 启动时,它的 HTTP 接收器会监听多个路径,因此无论你使用 /api/v1/traces 还是 /v1/traces,数据都能被正确接收。
  1. 为什么 /api/v1/traces 可以工作?
  • Jaeger 的兼容性设计:
    Jaeger 为了兼容不同客户端(如 OpenTelemetry、Jaeger 原生客户端等),支持多种路径。
    即使你显式指定 /api/v1/traces,Jaeger 仍然能够识别并处理数据。
  • OpenTelemetry 的灵活性:
    OpenTelemetry 的 OTLP 导出器允许你指定完整的 URL(包括路径),例如 http://localhost:4318/api/v1/traces。
    当你指定路径时,OTLP 导出器会直接将数据发送到该路径,而不会覆盖默认路径。
关于路径:/api/v1/traces

不同的可观测性后端如 Jaeger、Zipkin、Prometheus 都支持这一标准端点,实现了遥测数据的统一收集。

  • /api/v1/traces 是 Jaeger 的 HTTP 接收器的默认路径,通常用于直接使用 Jaeger 客户端的项目。
  • /v1/traces 是 OTLP 接收器的默认路径,是 OpenTelemetry 的通用协议。
    推荐使用 OTLP,OpenTelemetry 正在逐步成为可观测性领域的事实标准。因为它更通用且支持多种后端。如果你看到 /api/v1/traces,可能是因为项目直接使用了 Jaeger 客户端或旧版配置。

Jaeger 提供了两种主要的接收器来接收追踪数据:

  • Jaeger 的 HTTP 接收器:
    默认路径为 /api/v1/traces。
    这是一种专为 Jaeger 设计的接收器,主要用于接收 Jaeger 客户端发送的 JSON 或 Thrift 格式的追踪数据。
    例如,Jaeger 的 Go 客户端会通过 HTTP POST 请求将数据发送到 http:///api/v1/traces。

  • OTLP 接收器:
    默认路径为 /v1/traces。
    这是 OpenTelemetry 的通用协议(OTLP),支持通过 gRPC 或 HTTP 传输数据。
    使用 OTLP 时,客户端只需要指定 endpoint 的主机和端口,路径由协议自动处理

我们可以参考阿里日志服务文档 https://help.aliyun.com/zh/sls/user-guide/import-trace-data-from-opentelemetry-to-log-service,可以看到使用的 /v1/traces 路径。

接入点信息

  • HTTPS协议的接入点为${endpoint}/opentelemetry/v1/traces,例如https://test-project.cn-hangzhou-intranet.log.aliyuncs.com/opentelemetry/v1/traces。

  • gRPC协议的接入点为${endpoint}:10010,例如test-project.cn-hangzhou-intranet.log.aliyuncs.com:10010。

其他参考

通过OpenTelemetry监控gRPC调用
Monitor gRPC calls with OpenTelemetry - explained with a Golang example
https://dev.to/signoz/monitor-grpc-calls-with-opentelemetry-explained-with-a-golang-example-350o