fix(otelTrace): 增强span创建和结束的安全性

- 在Span和SpanWithMetrics函数中添加nil context检查,避免panic
- 优化CreateLinkContext函数,区分父span有效和无效时的处理逻辑
- 新增SafeEndSpan函数,安全结束span并捕获可能的panic
- 添加日志记录panic信息,防止程序崩溃
- 确保所有返回的context不为nil,提升稳定性
This commit is contained in:
danial
2025-12-14 18:21:52 +08:00
parent 54b49a4f06
commit bf8c478ade

View File

@@ -7,11 +7,17 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
// Span 创建并返回一个新的span优化了性能和指标收集
// 支持自动添加通用属性和错误处理
func Span(ctx context.Context, traceName, spanName string, attr ...trace.SpanStartOption) (context.Context, trace.Span) {
// 安全检查如果context为空创建一个默认的context以避免panic
if ctx == nil {
ctx = context.Background()
}
// 添加通用属性以优化指标收集
attrs := append([]trace.SpanStartOption{
trace.WithAttributes(
@@ -22,7 +28,7 @@ func Span(ctx context.Context, traceName, spanName string, attr ...trace.SpanSta
ctx, span := otel.Tracer(traceName).Start(ctx, spanName, attrs...)
// 如果context为空创建一个默认的context以避免panic
// 二次检查:确保返回的context不为nil
if ctx == nil {
ctx = context.Background()
}
@@ -41,6 +47,11 @@ func Span(ctx context.Context, traceName, spanName string, attr ...trace.SpanSta
// - 异步任务需要独立完成,不应被父请求取消影响
// - 后台处理任务需要保持与原始请求的追踪关联
func CreateLinkContext(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
// 安全检查确保context不为nil
if ctx == nil {
ctx = context.Background()
}
// 获取父span的上下文
parentSpanCtx := trace.SpanContextFromContext(ctx)
@@ -48,16 +59,27 @@ func CreateLinkContext(ctx context.Context, spanName string, opts ...trace.SpanS
// 使用Background()确保子ctx独立于父ctx的生命周期
linkCtx := trace.ContextWithSpanContext(context.Background(), parentSpanCtx)
// 创建新的span链接到父span
linkOpts := append(opts,
trace.WithLinks(trace.Link{SpanContext: parentSpanCtx}),
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("parent.trace_id", parentSpanCtx.TraceID().String()),
attribute.String("parent.span_id", parentSpanCtx.SpanID().String()),
attribute.String("context_type", "traceable"),
),
)
// 创建新的span链接到父span只在父span有效时添加链接
var linkOpts []trace.SpanStartOption
if parentSpanCtx.IsValid() {
linkOpts = append(opts,
trace.WithLinks(trace.Link{SpanContext: parentSpanCtx}),
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("parent.trace_id", parentSpanCtx.TraceID().String()),
attribute.String("parent.span_id", parentSpanCtx.SpanID().String()),
attribute.String("context_type", "traceable"),
),
)
} else {
// 父span无效时仍然创建span但不添加链接
linkOpts = append(opts,
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("context_type", "standalone"),
),
)
}
return otel.Tracer("link").Start(linkCtx, spanName, linkOpts...)
}
@@ -110,8 +132,36 @@ func AddSpanAttributes(span trace.Span, attrs ...attribute.KeyValue) {
span.SetAttributes(attrs...)
}
// SafeEndSpan 安全地结束span防止panic传播
// 建议在defer中使用defer SafeEndSpan(span)
func SafeEndSpan(span trace.Span) {
if span == nil {
return
}
// 使用defer+recover捕获可能的panic
defer func() {
if r := recover(); r != nil {
// 记录panic信息但不重新抛出
if Logger.logger != nil {
Logger.logger.Error("panic in span.End()",
zap.Any("panic", r),
zap.Stack("stack"),
)
}
}
}()
span.End()
}
// SpanWithMetrics 创建带有性能指标收集的span用于高频操作的性能监控
func SpanWithMetrics(ctx context.Context, traceName, spanName string, operationType string, attr ...trace.SpanStartOption) (context.Context, trace.Span) {
// 安全检查如果context为空创建一个默认的context以避免panic
if ctx == nil {
ctx = context.Background()
}
// 添加性能相关的属性
perfAttrs := append([]trace.SpanStartOption{
trace.WithAttributes(
@@ -124,6 +174,7 @@ func SpanWithMetrics(ctx context.Context, traceName, spanName string, operationT
ctx, span := otel.Tracer(traceName).Start(ctx, spanName, perfAttrs...)
// 二次检查确保返回的context不为nil
if ctx == nil {
ctx = context.Background()
}