微服务链路追踪要点记录

1.gorm 插件实现链路追踪 //通过 github.com/uptrace/opentelemetry-go-extra/otelgorm库可实现mysql-grom链路追踪

// 复制代码
	if o.enableTrace {
		err = db.Use(otelgorm.NewPlugin())
		if err != nil {
			return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
		}
	}

// gorm 插件demo learnku.com/docs/gorm/v2/write_plugins/9750#c5a38b 插件实现描述下: 当用户通过 db.Use(plugin) 注册插件时,GORM 会调用插件的 Initialize 方法,插件在此方法中完成核心功能的注册(如绑定回调、修改配置等)。

以after回调为例子

2.go-redis 实现的链路追踪 基于当前库: github.com/redis/go-redis/extra/redisotel/v9

// tracerProvider *trace.TracerProvider

rdb 复制代码
	if o.tracerProvider != nil {
		err = redisotel.InstrumentTracing(rdb, redisotel.WithTracerProvider(o.tracerProvider))
		if err != nil {
			return nil, err
		}
	}

3.grpc实现拦截追踪

scss 复制代码
func (s *grpcServer) unaryServerOptions() grpc.ServerOption {
	unaryServerInterceptors := []grpc.UnaryServerInterceptor{
		interceptor.UnaryServerRecovery(),
		interceptor.UnaryServerRequestID(),
	}
 ....       
// trace interceptor
	if config.Get().App.EnableTrace {
		streamServerInterceptors = append(streamServerInterceptors, interceptor.StreamServerTracing())
	}

	return grpc.ChainStreamInterceptor(streamServerInterceptors...)

4.http基于trace中间件处理 提取上游追踪上下文 使用cfg.Propagators.Extract()从HTTP请求头中提取上游服务传递过来的追踪上下文

创建或继续span 调用tracer.Start(ctx, spanName, tOpts...),这里的关键是:

如果提取到了上游追踪上下文,会继续现有的trace,创建子span 如果没有提取到上游上下文,才会创建新的trace

scss 复制代码
return func(c *gin.Context) {
		c.Set(tracerKey, tracer)
		savedCtx := c.Request.Context()
		defer func() {
			c.Request = c.Request.WithContext(savedCtx)
		}()
		ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header))
		route := c.FullPath()

		tOpts := []oteltrace.SpanStartOption{
			oteltrace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", c.Request)...),
			oteltrace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(c.Request)...),
			oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serviceName, route, c.Request)...),
			oteltrace.WithSpanKind(oteltrace.SpanKindServer),
		}
		spanName := route
		if spanName == "" {
			spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method)
		}
		ctx, span := tracer.Start(ctx, spanName, tOpts...)
		defer span.End()

		// pass the span through the request context
		c.Request = c.Request.WithContext(ctx)

		// serve the request to the next interceptor
		c.Next()

		status := c.Writer.Status()
		attrs := semconv.HTTPAttributesFromHTTPStatusCode(status)
		spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status)
		span.SetAttributes(attrs...)
		span.SetStatus(spanStatus, spanMessage)
		if len(c.Errors) > 0 {
			span.SetAttributes(attribute.String("gin.errors", c.Errors.String()))
		}
	}

基于gozero的简单封装 propagation.MapCarrier适合mq之间,kv传递 propagation.HeaderCarrier适合http、rpc之间,kv传递

kratos 链路追踪实现: juejin.cn/post/696828...

// go-zero :zhuanlan.zhihu.com/p/591180238

go 复制代码
package telemetry

import (
	"context"
	"os"
	"sync"

	zeroTrace "github.com/zeromicro/go-zero/core/trace"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/propagation"
	semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
	"go.opentelemetry.io/otel/trace"
)

var (
	resourcesOnce sync.Once
)

// https://zhuanlan.zhihu.com/p/591180238
// https://developer.aliyun.com/article/1597831
// 前提-gozero已经提前完成初始化操作

// TracingIt 给耗时的操作加上链路追踪。特别是需要调用外部服务的时候使用
// kind取值参考:trace.SpanKindProducer,上游决定是生产端,消费端,还是内部调用
func TracingIt(ctx context.Context, operation string, kind trace.SpanKind, fn func(ctx context.Context, span trace.Span) error, attrs ...attribute.KeyValue) error {
	tracer := otel.GetTracerProvider().Tracer(zeroTrace.TraceName)
	newCtx, span := tracer.Start(ctx, operation, trace.WithSpanKind(kind), trace.WithAttributes(attrs...))
	defer span.End()
	err := fn(newCtx, span)
	setSpanStatus(span, err)
	return err
}

// setSpanStatus 统一设置span状态
func setSpanStatus(span trace.Span, err error) {
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
	} else {
		span.SetStatus(codes.Ok, "ok")
	}
}

// NewSpan 创建一个新的span,支持自定义traceName和spanKind,这种方式记得defer span.End()
func NewSpan(ctx context.Context, operation string, opts ...SpanOption) (newCtx context.Context, span trace.Span) {
	options := defaultSpanOptions()
	for _, opt := range opts {
		opt(&options)
	}
	tracer := otel.GetTracerProvider().Tracer(options.traceName)
	return tracer.Start(ctx, operation, trace.WithSpanKind(options.spanKind), trace.WithAttributes(options.attrs...))
}

// SpanOption 用于自定义span参数
type SpanOption func(*spanOptions)

type spanOptions struct {
	traceName string
	spanKind  trace.SpanKind
	attrs     []attribute.KeyValue
}

func defaultSpanOptions() spanOptions {
	return spanOptions{
		traceName: zeroTrace.TraceName,
		spanKind:  trace.SpanKindProducer,
		attrs:     nil,
	}
}

func WithTraceName(name string) SpanOption {
	return func(o *spanOptions) {
		o.traceName = name
	}
}

func WithSpanKind(kind trace.SpanKind) SpanOption {
	return func(o *spanOptions) {
		o.spanKind = kind
	}
}

func WithAttributes(attrs ...attribute.KeyValue) SpanOption {
	return func(o *spanOptions) {
		o.attrs = attrs
	}
}

// InjectMapContext 将追踪上下文注入到 MapCarrier 中,支持自定义Propagator
func InjectMapContext(spanCtx context.Context, propagators ...propagation.TextMapPropagator) *propagation.MapCarrier {
	carrier := &propagation.MapCarrier{}
	propagator := otel.GetTextMapPropagator()
	if len(propagators) > 0 {
		propagator = propagators[0]
	}
	propagator.Inject(spanCtx, carrier)
	return carrier
}

// ExtractMapContext 从 MapCarrier 中提取追踪上下文,支持自定义Propagator
func ExtractMapContext(carrier propagation.MapCarrier, propagators ...propagation.TextMapPropagator) context.Context {
	propagator := otel.GetTextMapPropagator()
	if len(propagators) > 0 {
		propagator = propagators[0]
	}
	return propagator.Extract(context.Background(), carrier)
}

// InjectHeaderContext 将追踪上下文注入到 HeaderCarrier 中,支持自定义Propagator
func InjectHeaderContext(spanCtx context.Context, propagators ...propagation.TextMapPropagator) *propagation.HeaderCarrier {
	carrier := &propagation.HeaderCarrier{}
	propagator := otel.GetTextMapPropagator()
	if len(propagators) > 0 {
		propagator = propagators[0]
	}
	propagator.Inject(spanCtx, carrier)
	return carrier
}

// ExtractHeaderContext 从 HeaderCarrier 中提取追踪上下文,支持自定义Propagator
func ExtractHeaderContext(carrier propagation.HeaderCarrier, propagators ...propagation.TextMapPropagator) context.Context {
	propagator := otel.GetTextMapPropagator()
	if len(propagators) > 0 {
		propagator = propagators[0]
	}
	return propagator.Extract(context.Background(), carrier)
}

// AddResources 添加一些链路需要的字段,只初始化一次
func AddResources(serviceName string) {
	resourcesOnce.Do(func() {
		instanceId := os.Getenv("INSTANCE_ID")
		if instanceId == "" {
			instanceId = os.Getenv("POD_NAME")
		}
		if instanceId == "" {
			instanceId = serviceName
		}
		if instanceId == "" {
			instanceId = "unknown-instance"
		}
		zeroTrace.AddResources(semconv.ServiceInstanceID(instanceId))
		zeroTrace.AddResources(semconv.ServiceName(serviceName))
	})
}
相关推荐
Mintopia5 分钟前
每个国家的核安全是怎么保证的,都不怕在自己的领土爆炸吗?
前端·后端·面试
tonydf29 分钟前
浅聊一下AOP
后端
悟空码字31 分钟前
腾讯开源啦,源码地址+部署脚本
后端·腾讯
Xxtaoaooo37 分钟前
Spring Boot 启动卡死:循环依赖与Bean初始化的深度分析
java·后端·依赖注入·三级缓存机制·spring boot循环依赖
洛小豆1 小时前
Ubuntu 网络配置演进:从 20.04 到 24.04 的静态 IP 设置指南
linux·后端·ubuntu
JavaGuide1 小时前
2025 程序员时薪排行榜,PDD 太顶了!
java·后端
咖啡Beans1 小时前
Maven的POM常用标签详解
后端
MrSYJ1 小时前
别告诉我你还不会OAuth 2.0授权过滤器:OAuth2AuthorizationEndpointFilter第三篇
java·spring boot·后端
天天摸鱼的java工程师1 小时前
如何快速判断几十亿个数中是否存在某个数?—— 八年 Java 开发的实战避坑指南
java·后端
老邓计算机毕设2 小时前
Springboot乐家流浪猫管理系统16lxw(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端