package tracing import ( "net/http" "time" "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.26.0" "go.opentelemetry.io/otel/trace" ) // Transport wraps an http.RoundTripper with tracing. type Transport struct { base http.RoundTripper } // NewTransport creates a new tracing transport wrapper. func NewTransport(base http.RoundTripper) *Transport { if base == nil { base = http.DefaultTransport } return &Transport{base: base} } // RoundTrip implements http.RoundTripper with tracing. func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() tracer := otel.Tracer(tracerName) propagator := otel.GetTextMapPropagator() // Start client span spanName := "HTTP " + req.Method + " " + req.URL.Host + req.URL.Path ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes( semconv.HTTPRequestMethodKey.String(req.Method), semconv.URLFull(req.URL.String()), semconv.ServerAddress(req.URL.Host), attribute.String("upstream.name", req.URL.Host), ), ) defer span.End() // Inject trace context into outgoing request headers propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) // Perform the request start := time.Now() resp, err := t.base.RoundTrip(req.WithContext(ctx)) duration := time.Since(start) // Record timing span.SetAttributes(attribute.Float64("http.request.duration_ms", float64(duration.Milliseconds()))) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return nil, err } // Record response attributes span.SetAttributes( semconv.HTTPResponseStatusCode(resp.StatusCode), ) if resp.StatusCode >= 400 { span.SetStatus(codes.Error, http.StatusText(resp.StatusCode)) } else { span.SetStatus(codes.Ok, "") } return resp, nil }