654 lines
21 KiB
Go
654 lines
21 KiB
Go
package trace
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"math"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.opentelemetry.io/otel"
|
||
"go.opentelemetry.io/otel/propagation"
|
||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||
oteltrace "go.opentelemetry.io/otel/trace"
|
||
)
|
||
|
||
// initTracerSnapshot 捕获包 init() 后、任何测试改动前的全局 tracer 快照。
|
||
// 用于锁定 init() 的 Noop 兜底不变式(C13a),避免 resetGlobal/Init/Close
|
||
// 在其他测试中重置全局后掩盖 init() 路径的回归。
|
||
var initTracerSnapshot oteltrace.Tracer
|
||
|
||
func TestMain(m *testing.M) {
|
||
// init() 已运行;此处立即快照 getTracer()(永不 nil 的 Noop 兜底)。
|
||
initTracerSnapshot = getTracer()
|
||
code := m.Run()
|
||
os.Exit(code)
|
||
}
|
||
|
||
// resetGlobal 恢复 trace 包级全局到 Noop 兜底状态,避免测试间污染。
|
||
func resetGlobal(t *testing.T) {
|
||
t.Helper()
|
||
noopProvider := oteltrace.NewNoopTracerProvider()
|
||
noopTracer := noopProvider.Tracer("xlgo")
|
||
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample()))
|
||
tracerProviderPtr.Store(tp)
|
||
tracerPtr.Store(&noopTracer)
|
||
otel.SetTracerProvider(noopProvider)
|
||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||
propagation.TraceContext{}, propagation.Baggage{}))
|
||
}
|
||
|
||
func setOperationTimeoutForTest(t *testing.T, timeout time.Duration) {
|
||
t.Helper()
|
||
old := operationTimeoutNanos.Load()
|
||
operationTimeoutNanos.Store(int64(timeout))
|
||
t.Cleanup(func() {
|
||
operationTimeoutNanos.Store(old)
|
||
})
|
||
}
|
||
|
||
type blockingExporter struct{}
|
||
|
||
func (blockingExporter) ExportSpans(context.Context, []sdktrace.ReadOnlySpan) error {
|
||
return nil
|
||
}
|
||
|
||
func (blockingExporter) Shutdown(ctx context.Context) error {
|
||
<-ctx.Done()
|
||
return ctx.Err()
|
||
}
|
||
|
||
type recordingExporter struct {
|
||
mu sync.Mutex
|
||
spans []sdktrace.ReadOnlySpan
|
||
}
|
||
|
||
func (e *recordingExporter) ExportSpans(_ context.Context, spans []sdktrace.ReadOnlySpan) error {
|
||
e.mu.Lock()
|
||
defer e.mu.Unlock()
|
||
e.spans = append(e.spans, spans...)
|
||
return nil
|
||
}
|
||
|
||
func (e *recordingExporter) Shutdown(context.Context) error {
|
||
return nil
|
||
}
|
||
|
||
func (e *recordingExporter) Spans() []sdktrace.ReadOnlySpan {
|
||
e.mu.Lock()
|
||
defer e.mu.Unlock()
|
||
spans := make([]sdktrace.ReadOnlySpan, len(e.spans))
|
||
copy(spans, e.spans)
|
||
return spans
|
||
}
|
||
|
||
func requireNoPanic(t *testing.T, fn func()) {
|
||
t.Helper()
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
t.Fatalf("unexpected panic: %v", r)
|
||
}
|
||
}()
|
||
fn()
|
||
}
|
||
|
||
// TestC13aInitNoopInvariant 锁定 init() 的 Noop 兜底不变式:
|
||
// 包加载后(任何 Init/Close/resetGlobal 之前)getTracer() 必须非 nil。
|
||
//
|
||
// 变异 init() 去掉 Noop Store 后,initTracerSnapshot(在 TestMain 即 init 后捕获)
|
||
// 将为 nil → 此测试红。resetGlobal 等后续改动不影响此快照。
|
||
func TestC13aInitNoopInvariant(t *testing.T) {
|
||
if initTracerSnapshot == nil {
|
||
t.Fatal("init() did not store a Noop tracer: getTracer() was nil at package load (C13a)")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// C13a:未 Init 即用 → nil tracer panic
|
||
// ============================================================
|
||
|
||
// TestC13aNoInitNoPanic 验证未 Init 时包级函数不 panic(Noop 兜底)。
|
||
//
|
||
// 修复前:包级 tracer 为 nil,Middleware/StartSpanFromContext/GetTracer 裸用 → panic。
|
||
// 修复后:init() Store Noop,getTracer() 永不 nil。
|
||
func TestC13aNoInitNoPanic(t *testing.T) {
|
||
// 强制重置到"未 Init"的 Noop 兜底状态。
|
||
resetGlobal(t)
|
||
|
||
// GetTracer 非 nil。
|
||
if tr := GetTracer(); tr == nil {
|
||
t.Fatal("GetTracer() nil before Init (C13a)")
|
||
}
|
||
|
||
// StartSpanFromContext 不 panic。
|
||
ctx, span := StartSpanFromContext(context.Background(), "test-span")
|
||
defer span.End()
|
||
if span == nil {
|
||
t.Fatal("StartSpanFromContext returned nil span")
|
||
}
|
||
_ = ctx
|
||
|
||
// Middleware 不 panic:走一次请求。
|
||
gin.SetMode(gin.TestMode)
|
||
r := gin.New()
|
||
r.Use(Middleware("test-svc"))
|
||
r.GET("/p", func(c *gin.Context) {
|
||
// 下游用 c.Request.Context() 取 span(C13d 闭环)。
|
||
s := oteltrace.SpanFromContext(c.Request.Context())
|
||
c.String(http.StatusOK, s.SpanContext().TraceID().String())
|
||
})
|
||
|
||
w := httptest.NewRecorder()
|
||
req := httptest.NewRequest(http.MethodGet, "/p", nil)
|
||
r.ServeHTTP(w, req)
|
||
|
||
// Noop tracer 的 TraceID 为空(Noop 不记录),但绝不应 panic。
|
||
if w.Code != http.StatusOK {
|
||
t.Fatalf("status = %d, want 200 (C13a should not panic)", w.Code)
|
||
}
|
||
}
|
||
|
||
// TestC13aInitDisabledNoop 验证 Init(Enabled:false) 后 Noop 安全。
|
||
func TestC13aInitDisabledNoop(t *testing.T) {
|
||
t.Cleanup(func() { _ = Close(context.Background()); resetGlobal(t) })
|
||
if err := Init(Config{Enabled: false, ServiceName: "svc"}); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
if GetTracer() == nil {
|
||
t.Fatal("GetTracer nil after Init(Enabled:false)")
|
||
}
|
||
// Noop tracer Start 不 panic。
|
||
_, span := StartSpanFromContext(context.Background(), "x")
|
||
span.End()
|
||
}
|
||
|
||
// TestC13aCloseThenUseNoPanic 验证 Close 后再用不 panic(Store 回 Noop 兜底)。
|
||
func TestC13aCloseThenUseNoPanic(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
// 即便未真正 Init 出带 exporter 的 provider,Close 也应安全并把全局重置为兜底。
|
||
if err := Close(context.Background()); err != nil {
|
||
t.Fatalf("Close: %v", err)
|
||
}
|
||
// Close 后包级函数仍安全。
|
||
if GetTracer() == nil {
|
||
t.Fatal("GetTracer nil after Close")
|
||
}
|
||
_, span := StartSpanFromContext(context.Background(), "after-close")
|
||
span.End()
|
||
}
|
||
|
||
// ============================================================
|
||
// C13b:未知导出器返 nil + stdout 未实现
|
||
// ============================================================
|
||
|
||
// TestC13bStdoutExporterWorks 验证 stdout 导出器可创建(C13b 实现)。
|
||
func TestC13bStdoutExporterWorks(t *testing.T) {
|
||
// 用 stdouttrace 但 Init 会向 os.Stdout 输出,故直接测 createExporter。
|
||
cfg := Config{ExporterType: "stdout"}
|
||
exp, err := createExporter(cfg)
|
||
if err != nil {
|
||
t.Fatalf("createExporter(stdout): %v (C13b stdout unimplemented)", err)
|
||
}
|
||
if exp == nil {
|
||
t.Fatal("createExporter(stdout) returned nil exporter (C13b)")
|
||
}
|
||
_ = exp.Shutdown(context.Background())
|
||
}
|
||
|
||
// TestC13bUnknownExporterReturnsError 验证未知导出器返错(修复前返 nil,nil)。
|
||
func TestC13bUnknownExporterReturnsError(t *testing.T) {
|
||
cfg := Config{ExporterType: "xyz-unknown"}
|
||
exp, err := createExporter(cfg)
|
||
if err == nil {
|
||
t.Error("createExporter(unknown) should return error (C13b), got nil")
|
||
}
|
||
if exp != nil {
|
||
t.Errorf("createExporter(unknown) should return nil exporter, got %T", exp)
|
||
}
|
||
}
|
||
|
||
// TestC13bInitUnknownExporterFails 验证 Init 未知导出器返错(不喂 nil 给 WithBatcher)。
|
||
func TestC13bInitUnknownExporterFails(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
err := Init(Config{Enabled: true, ExporterType: "xyz-unknown", Propagator: "w3c"})
|
||
if err == nil {
|
||
t.Error("Init with unknown exporter should fail (C13b)")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// C13c:OTLP 默认 HTTPS 无 WithInsecure
|
||
// ============================================================
|
||
|
||
// TestC13cInsecureExporterCreates 验证 Insecure:true 时 otlp-http 导出器可创建(无 TLS 握手)。
|
||
// createExporter 仅构造 client,不连接;Insecure 注入不报错即验证 option 路径生效。
|
||
func TestC13cInsecureExporterCreates(t *testing.T) {
|
||
cfg := Config{ExporterType: "otlp-http", Endpoint: "localhost:4318", Insecure: true}
|
||
exp, err := createExporter(cfg)
|
||
if err != nil {
|
||
t.Fatalf("createExporter(otlp-http, Insecure): %v (C13c)", err)
|
||
}
|
||
_ = exp.Shutdown(context.Background())
|
||
}
|
||
|
||
// TestC13cOtlpGrpcInsecureCreates 验证 otlp-grpc Insecure 路径。
|
||
func TestC13cOtlpGrpcInsecureCreates(t *testing.T) {
|
||
cfg := Config{ExporterType: "otlp-grpc", Endpoint: "localhost:4317", Insecure: true}
|
||
exp, err := createExporter(cfg)
|
||
if err != nil {
|
||
t.Fatalf("createExporter(otlp-grpc, Insecure): %v (C13c)", err)
|
||
}
|
||
_ = exp.Shutdown(context.Background())
|
||
}
|
||
|
||
// ============================================================
|
||
// C13d:Middleware 不更新 c.Request
|
||
// ============================================================
|
||
|
||
// TestC13dRequestContextContainsSpan 验证 Middleware 更新 c.Request,
|
||
// 下游 c.Request.Context() 含 span(TraceID 非空且与 X-Trace-ID 一致)。
|
||
//
|
||
// 修复前:仅 c.Set("otel_ctx", ctx),下游 c.Request.Context() 无 span。
|
||
func TestC13dRequestContextContainsSpan(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { _ = Close(context.Background()); resetGlobal(t) })
|
||
|
||
// 用 stdout 导出器 + 全采样,使 span 真实生成(非 Noop)。
|
||
if err := Init(Config{
|
||
Enabled: true,
|
||
ServiceName: "test-svc",
|
||
ExporterType: "stdout",
|
||
SampleRatio: 1.0,
|
||
Propagator: "w3c",
|
||
}); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
|
||
gin.SetMode(gin.TestMode)
|
||
r := gin.New()
|
||
r.Use(Middleware("test-svc"))
|
||
|
||
var seenTraceID string
|
||
r.GET("/p", func(c *gin.Context) {
|
||
// 关键:用 c.Request.Context()(而非 trace.GetContext(c))取 span。
|
||
s := oteltrace.SpanFromContext(c.Request.Context())
|
||
seenTraceID = s.SpanContext().TraceID().String()
|
||
c.String(http.StatusOK, "ok")
|
||
})
|
||
|
||
w := httptest.NewRecorder()
|
||
req := httptest.NewRequest(http.MethodGet, "/p", nil)
|
||
r.ServeHTTP(w, req)
|
||
|
||
if seenTraceID == "" {
|
||
t.Fatal("c.Request.Context() has no span/TraceID (C13d: Middleware didn't update c.Request)")
|
||
}
|
||
headerTraceID := w.Header().Get("X-Trace-ID")
|
||
if headerTraceID == "" {
|
||
t.Fatal("X-Trace-ID header missing")
|
||
}
|
||
if seenTraceID != headerTraceID {
|
||
t.Errorf("TraceID mismatch: downstream %q vs header %q (C13d)", seenTraceID, headerTraceID)
|
||
}
|
||
}
|
||
|
||
// TestC13dPropagatedTraceContextExtracted 验证 Middleware 从入站 W3C 头提取父 context
|
||
// 并写入 c.Request,下游 span 继承父 TraceID。
|
||
func TestC13dPropagatedTraceContextExtracted(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { _ = Close(context.Background()); resetGlobal(t) })
|
||
|
||
if err := Init(Config{
|
||
Enabled: true, ServiceName: "test-svc", ExporterType: "stdout",
|
||
SampleRatio: 1.0, Propagator: "w3c",
|
||
}); err != nil {
|
||
t.Fatalf("Init: %v", err)
|
||
}
|
||
|
||
// 构造一个父 span 并注入 W3C traceparent 头(用真实采样 provider,非 noop)。
|
||
parentProvider := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
|
||
defer parentProvider.Shutdown(context.Background())
|
||
parentTracer := parentProvider.Tracer("test-parent")
|
||
parentCtx, parentSpan := parentTracer.Start(context.Background(), "parent")
|
||
defer parentSpan.End()
|
||
parentTraceID := parentSpan.SpanContext().TraceID().String()
|
||
|
||
carrier := propagation.HeaderCarrier{}
|
||
otel.GetTextMapPropagator().Inject(parentCtx, carrier)
|
||
|
||
gin.SetMode(gin.TestMode)
|
||
r := gin.New()
|
||
r.Use(Middleware("test-svc"))
|
||
var seen string
|
||
r.GET("/p", func(c *gin.Context) {
|
||
s := oteltrace.SpanFromContext(c.Request.Context())
|
||
seen = s.SpanContext().TraceID().String()
|
||
c.Status(http.StatusOK)
|
||
})
|
||
|
||
w := httptest.NewRecorder()
|
||
req := httptest.NewRequest(http.MethodGet, "/p", nil)
|
||
// 把 carrier 中的头复制到请求。
|
||
for _, key := range carrier.Keys() {
|
||
req.Header.Set(key, carrier.Get(key))
|
||
}
|
||
r.ServeHTTP(w, req)
|
||
|
||
if seen != parentTraceID {
|
||
t.Errorf("downstream TraceID = %q, want parent %q (C13d propagation/extract)", seen, parentTraceID)
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// C13e:b3/jaeger 未实现
|
||
// ============================================================
|
||
|
||
// TestC13eB3PropagatorImplemented 验证 b3 传播器返回非 nil 的 b3 propagator(C13e 实现)。
|
||
func TestC13eB3PropagatorImplemented(t *testing.T) {
|
||
prop, err := createPropagator("b3")
|
||
if err != nil {
|
||
t.Fatalf("createPropagator(b3): %v (C13e unimplemented)", err)
|
||
}
|
||
if prop == nil {
|
||
t.Fatal("createPropagator(b3) returned nil (C13e)")
|
||
}
|
||
// 用真实采样的 provider tracer,使 span SpanContext 有效。
|
||
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
|
||
defer tp.Shutdown(context.Background())
|
||
tracer := tp.Tracer("t")
|
||
ctx, span := tracer.Start(context.Background(), "s")
|
||
defer span.End()
|
||
// b3 propagator 应识别 b3 头。注入后 b3 单头或 x-b3-traceid 至少其一非空。
|
||
carrier := propagation.HeaderCarrier{}
|
||
prop.Inject(ctx, carrier)
|
||
if carrier.Get("b3") == "" && carrier.Get("x-b3-traceid") == "" {
|
||
t.Error("b3 propagator did not inject b3 headers (C13e)")
|
||
}
|
||
}
|
||
|
||
// TestC13eJaegerMapsToW3C 验证 jaeger 映射 W3C TraceContext(非静默 nil)。
|
||
func TestC13eJaegerMapsToW3C(t *testing.T) {
|
||
prop, err := createPropagator("jaeger")
|
||
if err != nil {
|
||
t.Fatalf("createPropagator(jaeger): %v (C13e)", err)
|
||
}
|
||
if prop == nil {
|
||
t.Fatal("createPropagator(jaeger) returned nil (C13e)")
|
||
}
|
||
// 用真实采样的 provider tracer,使 span SpanContext 有效(noop tracer 不生成 TraceID)。
|
||
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
|
||
defer tp.Shutdown(context.Background())
|
||
tracer := tp.Tracer("t")
|
||
ctx, span := tracer.Start(context.Background(), "s")
|
||
defer span.End()
|
||
// 注入 W3C traceparent 头。
|
||
carrier := propagation.HeaderCarrier{}
|
||
prop.Inject(ctx, carrier)
|
||
if carrier.Get("traceparent") == "" {
|
||
t.Error("jaeger(→W3C) propagator did not inject traceparent (C13e)")
|
||
}
|
||
}
|
||
|
||
// TestC13eUnknownPropagatorReturnsError 验证未知传播器返错(修复前静默回落 W3C)。
|
||
func TestC13eUnknownPropagatorReturnsError(t *testing.T) {
|
||
prop, err := createPropagator("xyz")
|
||
if err == nil {
|
||
t.Error("createPropagator(unknown) should return error (C13e), got nil")
|
||
}
|
||
if prop != nil {
|
||
t.Errorf("createPropagator(unknown) should return nil, got %T", prop)
|
||
}
|
||
}
|
||
|
||
// TestC13eInitUnknownPropagatorFails 验证 Init 未知传播器返错且回滚 provider。
|
||
func TestC13eInitUnknownPropagatorFails(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
err := Init(Config{Enabled: true, ExporterType: "stdout", Propagator: "xyz"})
|
||
if err == nil {
|
||
t.Error("Init with unknown propagator should fail (C13e)")
|
||
}
|
||
}
|
||
|
||
// TestC13eW3CDefault 验证空 propagator 默认 W3C(兼容)。
|
||
func TestC13eW3CDefault(t *testing.T) {
|
||
prop, err := createPropagator("")
|
||
if err != nil {
|
||
t.Fatalf("createPropagator(''): %v", err)
|
||
}
|
||
if prop == nil {
|
||
t.Fatal("createPropagator('') returned nil")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// C13 顺带:GetContext 裸断言防护
|
||
// ============================================================
|
||
|
||
// TestC13GetContextCommaOk 验证 otel_ctx 被置为非 context 值时不 panic。
|
||
func TestC13GetContextCommaOk(t *testing.T) {
|
||
gin.SetMode(gin.TestMode)
|
||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||
c.Set("otel_ctx", "not-a-context") // 污染
|
||
|
||
// 修复前:ctx.(context.Context) 裸断言 panic;修复后 comma-ok 回退 c.Request.Context()。
|
||
ctx := GetContext(c)
|
||
if ctx == nil {
|
||
t.Fatal("GetContext returned nil")
|
||
}
|
||
// 应回退到 c.Request.Context()。
|
||
if ctx != c.Request.Context() {
|
||
t.Error("GetContext should fall back to c.Request.Context() on bad otel_ctx")
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// H-14:Close→Init→Close 多轮生命周期(原 sync.Once 致第二次 Close 不 Shutdown)
|
||
// ============================================================
|
||
|
||
// TestH14CloseInitCloseMultipleRounds 验证 Close 可反复执行,
|
||
// 且 Close→Init→Close 第二次 Close 仍真正 Shutdown 新 provider(不泄漏 exporter)。
|
||
//
|
||
// 修复前:Close 用 sync.Once,第二次 Close no-op,第二轮 Init 创建的 provider 的
|
||
// exporter 后台 goroutine/连接泄漏。
|
||
func TestH14CloseInitCloseMultipleRounds(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
for round := 0; round < 3; round++ {
|
||
if err := Init(Config{Enabled: true, ServiceName: "svc", ExporterType: "stdout", Propagator: "w3c"}); err != nil {
|
||
t.Fatalf("round %d Init failed: %v", round, err)
|
||
}
|
||
// Init 后 provider 非 nil
|
||
if TracerProvider() == nil {
|
||
t.Fatalf("round %d: provider nil after Init", round)
|
||
}
|
||
if err := Close(context.Background()); err != nil {
|
||
t.Fatalf("round %d Close failed: %v", round, err)
|
||
}
|
||
// Close 后仍非 nil(兜底 NeverSample provider,C13a)
|
||
if TracerProvider() == nil {
|
||
t.Fatalf("round %d: provider nil after Close", round)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestH14CloseIdempotent 验证连续 Close 不 panic、不报错。
|
||
func TestH14CloseIdempotent(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
if err := Close(context.Background()); err != nil {
|
||
t.Fatalf("first Close: %v", err)
|
||
}
|
||
if err := Close(context.Background()); err != nil {
|
||
t.Fatalf("second Close: %v", err)
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// M14 trace residue: validation, timeout, nil guards, headers, service name
|
||
// ============================================================
|
||
func TestM14SampleRatioRange(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
for _, ratio := range []float64{-0.01, 1.01, math.NaN()} {
|
||
err := Init(Config{
|
||
Enabled: true,
|
||
ExporterType: "stdout",
|
||
Propagator: "w3c",
|
||
SampleRatio: ratio,
|
||
})
|
||
if err == nil {
|
||
t.Fatalf("Init with SampleRatio=%v should fail", ratio)
|
||
}
|
||
}
|
||
|
||
for _, ratio := range []float64{0, 1} {
|
||
if err := Init(Config{
|
||
Enabled: true,
|
||
ServiceName: "svc",
|
||
ExporterType: "stdout",
|
||
Propagator: "w3c",
|
||
SampleRatio: ratio,
|
||
}); err != nil {
|
||
t.Fatalf("Init with boundary SampleRatio=%v failed: %v", ratio, err)
|
||
}
|
||
if err := Close(context.Background()); err != nil {
|
||
t.Fatalf("Close after SampleRatio=%v: %v", ratio, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestM14CloseUsesOperationTimeout(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
setOperationTimeoutForTest(t, 20*time.Millisecond)
|
||
|
||
blockingProvider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(blockingExporter{}))
|
||
tracerProviderPtr.Store(blockingProvider)
|
||
|
||
start := time.Now()
|
||
err := Close(context.Background())
|
||
elapsed := time.Since(start)
|
||
if !errors.Is(err, context.DeadlineExceeded) {
|
||
t.Fatalf("Close error = %v, want context deadline exceeded", err)
|
||
}
|
||
if elapsed > 500*time.Millisecond {
|
||
t.Fatalf("Close took %s, expected operation timeout to bound shutdown", elapsed)
|
||
}
|
||
}
|
||
|
||
func TestM14InitDisabledShutdownUsesOperationTimeout(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
setOperationTimeoutForTest(t, 20*time.Millisecond)
|
||
|
||
blockingProvider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(blockingExporter{}))
|
||
tracerProviderPtr.Store(blockingProvider)
|
||
|
||
start := time.Now()
|
||
if err := Init(Config{Enabled: false, ServiceName: "svc"}); err != nil {
|
||
t.Fatalf("Init disabled: %v", err)
|
||
}
|
||
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
|
||
t.Fatalf("Init disabled took %s, expected operation timeout to bound old provider shutdown", elapsed)
|
||
}
|
||
}
|
||
|
||
func TestM14RecordErrorNilInputsNoPanic(t *testing.T) {
|
||
gin.SetMode(gin.TestMode)
|
||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||
|
||
requireNoPanic(t, func() { RecordError(nil, errors.New("boom")) })
|
||
requireNoPanic(t, func() { RecordError(c, nil) })
|
||
|
||
var nilSpan oteltrace.Span
|
||
requireNoPanic(t, func() { RecordErrorToSpan(nilSpan, errors.New("boom")) })
|
||
requireNoPanic(t, func() {
|
||
RecordErrorToSpan(oteltrace.SpanFromContext(context.Background()), nil)
|
||
})
|
||
}
|
||
|
||
func TestM14NoZeroTraceIDHeaderFromNoop(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
gin.SetMode(gin.TestMode)
|
||
r := gin.New()
|
||
r.Use(Middleware("svc"))
|
||
r.GET("/p", func(c *gin.Context) {
|
||
c.Status(http.StatusOK)
|
||
})
|
||
|
||
w := httptest.NewRecorder()
|
||
req := httptest.NewRequest(http.MethodGet, "/p", nil)
|
||
r.ServeHTTP(w, req)
|
||
|
||
const zeroTraceID = "00000000000000000000000000000000"
|
||
if got := w.Header().Get("X-Trace-ID"); got == zeroTraceID {
|
||
t.Fatalf("X-Trace-ID should not expose all-zero TraceID, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestM14MiddlewareServiceNameAttribute(t *testing.T) {
|
||
resetGlobal(t)
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
exporter := &recordingExporter{}
|
||
provider := sdktrace.NewTracerProvider(
|
||
sdktrace.WithSyncer(exporter),
|
||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||
)
|
||
tracer := provider.Tracer("test")
|
||
tracerProviderPtr.Store(provider)
|
||
tracerPtr.Store(&tracer)
|
||
|
||
gin.SetMode(gin.TestMode)
|
||
r := gin.New()
|
||
r.Use(Middleware("middleware-svc"))
|
||
r.GET("/p", func(c *gin.Context) {
|
||
c.Status(http.StatusOK)
|
||
})
|
||
|
||
w := httptest.NewRecorder()
|
||
req := httptest.NewRequest(http.MethodGet, "/p", nil)
|
||
r.ServeHTTP(w, req)
|
||
|
||
for _, span := range exporter.Spans() {
|
||
for _, attr := range span.Attributes() {
|
||
if string(attr.Key) == "service.name" && attr.Value.AsString() == "middleware-svc" {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
t.Fatal("server span missing service.name attribute from Middleware(serviceName)")
|
||
}
|
||
|
||
// TestM65PropagatorFailureLeavesOtelUsable 验证传播器失败时 otel 全局未被切到
|
||
// 已 Shutdown 的 provider(M-65)。失败后仍可正常 start span 不 panic。
|
||
func TestM65PropagatorFailureLeavesOtelUsable(t *testing.T) {
|
||
t.Cleanup(func() { resetGlobal(t) })
|
||
|
||
// 先成功 Init 一个
|
||
if err := Init(Config{Enabled: true, ServiceName: "svc", ExporterType: "stdout", Propagator: "w3c"}); err != nil {
|
||
t.Fatalf("setup Init: %v", err)
|
||
}
|
||
|
||
// 再用非法传播器 Init,应失败
|
||
err := Init(Config{Enabled: true, ExporterType: "stdout", Propagator: "xyz"})
|
||
if err == nil {
|
||
t.Fatal("Init with bad propagator should fail")
|
||
}
|
||
|
||
// otel 全局 provider 仍可用:StartSpan 不 panic
|
||
tr := otel.Tracer("test")
|
||
_, span := tr.Start(context.Background(), "after-failure")
|
||
span.End()
|
||
}
|