微服务链路追踪要点记录

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))
	})
}
相关推荐
Mike_小新14 分钟前
【Mike随想】未来更看重架构能力和业务经验,而非单纯编码能力
后端·程序员
Abadbeginning17 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
很小心的小新22 分钟前
五、SpringBoot工程打包与运行
java·spring boot·后端
ACGkaka_25 分钟前
SpringBoot 集成 MapStruct
java·spring boot·后端
anthem3725 分钟前
12、Python项目实战
后端
anthem3725 分钟前
7、Python高级特性 - 提升代码质量与效率
后端
anthem3725 分钟前
6、Python文件操作与异常处理
后端
anthem3729 分钟前
3、Python控制流与函数 - 从Java到Python的转变
后端
pe7er1 小时前
Mac 上使用 Homebrew 安装 MySQL 8.4 和 MySQL 5.7 共存
前端·后端
coding随想1 小时前
数据库里的“锁”事:共享锁、排他锁与三级封锁协议
后端