@@ -154,13 +154,13 @@ func main() {
@@ -192,22 +192,22 @@ func main() {
diff --git a/doc.go b/doc.go index 4d5595b..f984741 100644 --- a/doc.go +++ b/doc.go @@ -2,10 +2,9 @@ // features such as collecting performance data, identifying where requests // spend most of their time, and segmenting requests. // -// Traces are created using OpenTracing APIs and exported via the configured -// global tracer (opentracing.GlobalTracer). The core package configures this -// tracer at startup — typically an OpenTelemetry bridge that sends traces to -// any OTLP-compatible backend (Jaeger, Grafana Tempo, Honeycomb, etc.) or +// Traces are created and exported via OpenTelemetry. The core package +// configures the OTEL tracer provider at startup, sending traces to any +// OTLP-compatible backend (Jaeger, Grafana Tempo, Honeycomb, etc.) or // New Relic. package tracing diff --git a/go.mod b/go.mod index 983e1e0..c4e1ce6 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.25.8 require ( github.com/newrelic/go-agent/v3 v3.42.0 - github.com/opentracing/opentracing-go v1.2.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 google.golang.org/grpc v1.79.3 ) @@ -81,6 +82,8 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-git/go-git/v5 v5.17.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -226,6 +229,8 @@ require ( go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 283e8df..bda716a 100644 --- a/go.sum +++ b/go.sum @@ -232,6 +232,7 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -524,8 +525,6 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -718,16 +717,16 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= diff --git a/tracing.go b/tracing.go index 735f4e6..59bb5a3 100644 --- a/tracing.go +++ b/tracing.go @@ -2,20 +2,45 @@ package tracing import ( "context" - "encoding/base64" + "fmt" "net/http" "runtime/trace" "strings" nrutil "github.com/go-coldbrew/tracing/newrelic" newrelic "github.com/newrelic/go-agent/v3/newrelic" - opentracing "github.com/opentracing/opentracing-go" - otext "github.com/opentracing/opentracing-go/ext" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + oteltrace "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/metadata" ) -// Span defines an interface for implementing a tracing span -// This is used to abstract the underlying tracing implementation, currently using opentracing/opentelemetry and newrelic tracing libraries for implementation +const tracerName = "github.com/go-coldbrew/tracing" + +// toAttribute converts a key-value pair to a typed OTEL attribute, +// preserving numeric and boolean types instead of stringifying everything. +func toAttribute(key string, value interface{}) attribute.KeyValue { + switch v := value.(type) { + case string: + return attribute.String(key, v) + case int: + return attribute.Int(key, v) + case int64: + return attribute.Int64(key, v) + case float64: + return attribute.Float64(key, v) + case bool: + return attribute.Bool(key, v) + default: + return attribute.String(key, fmt.Sprint(v)) + } +} + +// Span defines an interface for implementing a tracing span. +// Consumers use this to create and annotate spans without coupling to a +// specific tracing backend. type Span interface { // End ends the span, can also use Finish() End() @@ -30,7 +55,7 @@ type Span interface { } type tracingSpan struct { - openSpan opentracing.Span + otelSpan oteltrace.Span datastore bool external bool dataSegment newrelic.DatastoreSegment @@ -42,10 +67,9 @@ type tracingSpan struct { func (span *tracingSpan) End() { if span == nil { - // dont panic when called against a nil span return } - span.openSpan.Finish() + span.otelSpan.End() if span.datastore { span.dataSegment.End() @@ -68,10 +92,9 @@ func (span *tracingSpan) Finish() { func (span *tracingSpan) SetTag(key string, value interface{}) { if span == nil { - // dont panic when called against a nil span return } - span.openSpan.SetTag(key, value) + span.otelSpan.SetAttributes(toAttribute(key, value)) if span.datastore { span.dataSegment.AddAttribute(key, value) } else if span.external { @@ -83,10 +106,9 @@ func (span *tracingSpan) SetTag(key string, value interface{}) { func (span *tracingSpan) SetQuery(query string) { if span == nil { - // dont panic when called against a nil span return } - span.openSpan.SetTag("query", query) + span.otelSpan.SetAttributes(toAttribute("query", query)) if span.datastore { span.dataSegment.ParameterizedQuery = query } @@ -94,11 +116,10 @@ func (span *tracingSpan) SetQuery(query string) { func (span *tracingSpan) SetError(err error) error { if span == nil || err == nil { - // dont panic when called against a nil span return err } - span.openSpan.SetTag("error", "true") - span.openSpan.SetTag("errorDetails", err.Error()) + span.otelSpan.SetStatus(codes.Error, err.Error()) + span.otelSpan.RecordError(err) if span.datastore { span.dataSegment.AddAttribute("error", err) } else if span.external { @@ -113,10 +134,10 @@ func (span *tracingSpan) SetError(err error) error { return err } -// NewInternalSpan starts a span for tracing internal actions -// This is used to trace actions within the same service, for example, a function call within the same service +// NewInternalSpan starts a span for tracing internal actions. +// This is used to trace actions within the same service, for example, a function call. func NewInternalSpan(ctx context.Context, name string) (Span, context.Context) { - zip, ctx := opentracing.StartSpanFromContext(ctx, name) + ctx, otelSpan := otel.Tracer(tracerName).Start(ctx, name) txnStarted := false txn := nrutil.GetNewRelicTransactionFromContext(ctx) @@ -132,7 +153,7 @@ func NewInternalSpan(ctx context.Context, name string) (Span, context.Context) { } reg := trace.StartRegion(ctx, name) span := &tracingSpan{ - openSpan: zip, + otelSpan: otelSpan, segment: seg, runtimeRegion: reg, } @@ -142,17 +163,21 @@ func NewInternalSpan(ctx context.Context, name string) (Span, context.Context) { return span, ctx } -// NewDatastoreSpan starts a span for tracing data store actions -// This is used to trace actions against a data store, for example, a database query or a redis call +// NewDatastoreSpan starts a span for tracing data store actions. +// This is used to trace actions against a data store, for example, a database query or a redis call. func NewDatastoreSpan(ctx context.Context, datastore, operation, collection string) (Span, context.Context) { name := operation if !strings.HasPrefix(name, datastore) { name = datastore + name } - zip, ctx := opentracing.StartSpanFromContext(ctx, name) - zip.SetTag("store", datastore) - zip.SetTag("collection", collection) - zip.SetTag("operation", operation) + ctx, otelSpan := otel.Tracer(tracerName).Start(ctx, name, + oteltrace.WithSpanKind(oteltrace.SpanKindClient), + ) + otelSpan.SetAttributes( + attribute.String("store", datastore), + attribute.String("collection", collection), + attribute.String("operation", operation), + ) txnStarted := false txn := nrutil.GetNewRelicTransactionFromContext(ctx) @@ -170,7 +195,7 @@ func NewDatastoreSpan(ctx context.Context, datastore, operation, collection stri } reg := trace.StartRegion(ctx, name) span := &tracingSpan{ - openSpan: zip, + otelSpan: otelSpan, dataSegment: seg, datastore: true, runtimeRegion: reg, @@ -182,7 +207,7 @@ func NewDatastoreSpan(ctx context.Context, datastore, operation, collection stri } func buildExternalSpan(ctx context.Context, name string, url string) (*tracingSpan, context.Context) { - ctx, zip := ClientSpan(name, ctx) + ctx, clientSpan := clientSpanOTEL(ctx, name) if !strings.HasPrefix(url, "/") { url = "/" + url @@ -191,7 +216,7 @@ func buildExternalSpan(ctx context.Context, name string, url string) (*tracingSp url = "http://" + name + "/" + url } - zip.SetTag("url", url) + clientSpan.SetAttributes(attribute.String("url", url)) txnStarted := false txn := nrutil.GetNewRelicTransactionFromContext(ctx) if txn == nil { @@ -206,7 +231,7 @@ func buildExternalSpan(ctx context.Context, name string, url string) (*tracingSp } reg := trace.StartRegion(ctx, name) span := &tracingSpan{ - openSpan: zip, + otelSpan: clientSpan, externalSegment: seg, external: true, runtimeRegion: reg, @@ -217,97 +242,84 @@ func buildExternalSpan(ctx context.Context, name string, url string) (*tracingSp return span, ctx } -// NewExternalSpan starts a span for tracing external actions -// This is used to trace actions against an external service, for example, a call to another service or a call to an external API +// NewExternalSpan starts a span for tracing external actions. +// This is used to trace actions against an external service. func NewExternalSpan(ctx context.Context, name string, url string) (Span, context.Context) { return buildExternalSpan(ctx, name, url) } -// NewHTTPExternalSpan starts a span for tracing external HTTP actions -// This is used to trace actions against an external service, for example, a call to another service or a call to an external API -// It also adds the HTTP headers to the span so that the external service can trace the call back to this service if needed +// NewHTTPExternalSpan starts a span for tracing external HTTP actions. +// It also injects trace propagation headers so the external service can +// correlate the call back to this service. func NewHTTPExternalSpan(ctx context.Context, name string, url string, hdr http.Header) (Span, context.Context) { s, ctx := buildExternalSpan(ctx, name, url) - traceHTTPHeaders(ctx, s.openSpan, hdr) - return s, ctx -} - -func traceHTTPHeaders(ctx context.Context, sp opentracing.Span, hdr http.Header) { - // Transmit the span's TraceContext as HTTP headers on our - // outbound request. - // Best-effort trace propagation — inject errors are non-fatal - _ = opentracing.GlobalTracer().Inject( - sp.Context(), - opentracing.HTTPHeaders, - opentracing.HTTPHeadersCarrier(hdr)) -} - -// A type that conforms to opentracing.TextMapReader and -// opentracing.TextMapWriter. -type metadataReaderWriter struct { - *metadata.MD -} - -func (w metadataReaderWriter) Set(key, val string) { - key = strings.ToLower(key) - if strings.HasSuffix(key, "-bin") { - val = string(base64.StdEncoding.EncodeToString([]byte(val))) + if hdr != nil { + otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(hdr)) } - (*w.MD)[key] = append((*w.MD)[key], val) + return s, ctx } -func (w metadataReaderWriter) ForeachKey(handler func(key, val string) error) error { - for k, vals := range *w.MD { - for _, v := range vals { - if err := handler(k, v); err != nil { - return err - } - } - } - return nil +// clientSpanOTEL starts a new client span linked to any existing span in context. +func clientSpanOTEL(ctx context.Context, operationName string) (context.Context, oteltrace.Span) { + return otel.Tracer(tracerName).Start(ctx, operationName, + oteltrace.WithSpanKind(oteltrace.SpanKindClient), + ) } // ClientSpan starts a new client span linked to the existing spans if any are found -// in the context. The returned context should be used in place of the original -func ClientSpan(operationName string, ctx context.Context) (context.Context, opentracing.Span) { - tracer := opentracing.GlobalTracer() - var clientSpan opentracing.Span - if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil { - clientSpan = tracer.StartSpan( - operationName, - opentracing.ChildOf(parentSpan.Context()), - ) - } else { - clientSpan = tracer.StartSpan(operationName) - } - otext.SpanKindRPCClient.Set(clientSpan) - ctx = opentracing.ContextWithSpan(ctx, clientSpan) - return ctx, clientSpan +// in the context. The returned context should be used in place of the original. +func ClientSpan(operationName string, ctx context.Context) (context.Context, oteltrace.Span) { + return clientSpanOTEL(ctx, operationName) } -// GRPCTracingSpan starts a new client span linked to the existing spans if any are found -// in the context. The returned context should be used in place of the original +// GRPCTracingSpan starts a new server span from incoming gRPC metadata. +// The returned context should be used in place of the original. func GRPCTracingSpan(operationName string, ctx context.Context) context.Context { - tracer := opentracing.GlobalTracer() - // Retrieve gRPC metadata. + // Extract trace context from incoming gRPC metadata. md, ok := metadata.FromIncomingContext(ctx) if ok { md = md.Copy() } else { md = metadata.MD{} } - if span := opentracing.SpanFromContext(ctx); span != nil { - _ = tracer.Inject(span.Context(), opentracing.TextMap, metadataReaderWriter{&md}) + + // Extract propagated trace context from metadata. + prop := otel.GetTextMapPropagator() + ctx = prop.Extract(ctx, metadataCarrier(md)) + + // Start a server span (automatically linked to extracted parent). + ctx, _ = otel.Tracer(tracerName).Start(ctx, operationName, + oteltrace.WithSpanKind(oteltrace.SpanKindServer), + ) + + // Preserve existing outgoing metadata and inject trace context into it. + outMD, _ := metadata.FromOutgoingContext(ctx) + outMD = outMD.Copy() + prop.Inject(ctx, metadataCarrier(outMD)) + ctx = metadata.NewOutgoingContext(ctx, outMD) + return ctx +} + +// metadataCarrier adapts gRPC metadata.MD to propagation.TextMapCarrier. +type metadataCarrier metadata.MD + +func (mc metadataCarrier) Get(key string) string { + vals := metadata.MD(mc).Get(key) + if len(vals) == 0 { + return "" } + // Join multiple values for W3C baggage/tracestate compatibility. + return strings.Join(vals, ",") +} - var span opentracing.Span - wireContext, err := tracer.Extract(opentracing.TextMap, metadataReaderWriter{&md}) - if err != nil { - span = tracer.StartSpan(operationName) - } else { - span = tracer.StartSpan(operationName, otext.RPCServerOption(wireContext)) +func (mc metadataCarrier) Set(key, value string) { + metadata.MD(mc).Set(key, value) +} + +func (mc metadataCarrier) Keys() []string { + keys := make([]string, 0, len(mc)) + for k := range mc { + keys = append(keys, k) } - ctx = opentracing.ContextWithSpan(ctx, span) - ctx = metadata.NewOutgoingContext(ctx, md) - return ctx + return keys } diff --git a/tracing_test.go b/tracing_test.go index cbd2739..8276436 100644 --- a/tracing_test.go +++ b/tracing_test.go @@ -3,6 +3,7 @@ package tracing import ( "context" "errors" + "net/http" "testing" "google.golang.org/grpc/metadata" @@ -17,7 +18,8 @@ func TestNewInternalSpan(t *testing.T) { if newCtx == nil { t.Fatal("expected non-nil context") } - // Verify the span can be ended without panic. + // With the noop tracer, the span context won't be valid, + // but we verify a span exists and can be ended without panic. span.End() } @@ -30,7 +32,6 @@ func TestNewDatastoreSpan(t *testing.T) { if newCtx == nil { t.Fatal("expected non-nil context") } - // Set a query to exercise the datastore-specific path. span.SetQuery("GET users:123") span.SetTag("key", "users:123") span.End() @@ -63,61 +64,28 @@ func TestSpanNilSafety(t *testing.T) { t.Fatal("expected SetError to return the provided error even on nil span") } - // SetError with nil error should also be safe. err = span.SetError(nil) if err != nil { t.Fatal("expected SetError(nil) to return nil") } } -func TestMetadataReaderWriter(t *testing.T) { +func TestMetadataCarrier(t *testing.T) { md := metadata.MD{} - rw := metadataReaderWriter{&md} + mc := metadataCarrier(md) - // Test Set with a normal key. - rw.Set("X-Request-ID", "abc123") - vals := md["x-request-id"] - if len(vals) != 1 || vals[0] != "abc123" { - t.Fatalf("expected [abc123], got %v", vals) + mc.Set("x-request-id", "abc123") + if got := mc.Get("x-request-id"); got != "abc123" { + t.Fatalf("expected abc123, got %s", got) } - // Test Set with a "-bin" suffix key triggers base64 encoding. - rw.Set("X-Data-Bin", "hello") - vals = md["x-data-bin"] - if len(vals) != 1 || vals[0] != "aGVsbG8=" { - t.Fatalf("expected [aGVsbG8=] for bin key, got %v", vals) + if got := mc.Get("nonexistent"); got != "" { + t.Fatalf("expected empty string for missing key, got %s", got) } - // Test that Set appends on repeated calls. - rw.Set("X-Request-ID", "def456") - vals = md["x-request-id"] - if len(vals) != 2 { - t.Fatalf("expected 2 values, got %d", len(vals)) - } - - // Test ForeachKey iterates all key-value pairs. - collected := make(map[string][]string) - err := rw.ForeachKey(func(key, val string) error { - collected[key] = append(collected[key], val) - return nil - }) - if err != nil { - t.Fatalf("unexpected error from ForeachKey: %v", err) - } - if len(collected["x-request-id"]) != 2 { - t.Fatalf("expected 2 values for x-request-id, got %d", len(collected["x-request-id"])) - } - if len(collected["x-data-bin"]) != 1 { - t.Fatalf("expected 1 value for x-data-bin, got %d", len(collected["x-data-bin"])) - } - - // Test ForeachKey propagates handler errors. - expectedErr := errors.New("stop") - err = rw.ForeachKey(func(key, val string) error { - return expectedErr - }) - if !errors.Is(err, expectedErr) { - t.Fatalf("expected ForeachKey to return handler error, got %v", err) + keys := mc.Keys() + if len(keys) != 1 || keys[0] != "x-request-id" { + t.Fatalf("expected [x-request-id], got %v", keys) } } @@ -130,7 +98,7 @@ func TestClientSpan(t *testing.T) { if newCtx == nil { t.Fatal("expected non-nil context from ClientSpan") } - span.Finish() + span.End() // Test with an existing parent span in context. ctxWithSpan, parentSpan := ClientSpan("parent-operation", ctx) @@ -141,23 +109,36 @@ func TestClientSpan(t *testing.T) { if childCtx == nil { t.Fatal("expected non-nil child context") } - childSpan.Finish() - parentSpan.Finish() + childSpan.End() + parentSpan.End() } func TestGRPCTracingSpan(t *testing.T) { ctx := context.Background() - // Test with no existing span in context newCtx := GRPCTracingSpan("test-operation", ctx) if newCtx == nil { t.Fatal("expected non-nil context") } - // Test with existing span in context (via ClientSpan) + // Test with existing span in context (via ClientSpan). ctxWithSpan, span := ClientSpan("parent-op", ctx) - defer span.Finish() + defer span.End() newCtx2 := GRPCTracingSpan("child-operation", ctxWithSpan) if newCtx2 == nil { t.Fatal("expected non-nil context with parent span") } } + +func TestNewHTTPExternalSpan(t *testing.T) { + ctx := context.Background() + hdr := make(http.Header) + span, newCtx := NewHTTPExternalSpan(ctx, "external-svc", "/api/data", hdr) + if span == nil { + t.Fatal("expected non-nil span") + } + if newCtx == nil { + t.Fatal("expected non-nil context") + } + // Headers may contain trace propagation if a real propagator is configured. + span.End() +}