diff --git a/rpc/http.go b/rpc/http.go index a74f36a1b0..55f0abfa72 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -30,6 +30,9 @@ import ( "strconv" "sync" "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) const ( @@ -334,6 +337,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo) + // Extract trace context from incoming headers. + ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header)) + // All checks passed, create a codec that reads directly from the request body // until EOF, writes the response to w, and orders the server to process a // single request. diff --git a/rpc/tracing_test.go b/rpc/tracing_test.go index 89cd31a075..f32a647e6f 100644 --- a/rpc/tracing_test.go +++ b/rpc/tracing_test.go @@ -21,7 +21,9 @@ import ( "net/http/httptest" "testing" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" ) @@ -60,16 +62,33 @@ func newTracingServer(t *testing.T) (*Server, *sdktrace.TracerProvider, *tracete // TestTracingHTTP verifies that RPC spans are emitted when processing HTTP requests. func TestTracingHTTP(t *testing.T) { - t.Parallel() + // Not parallel: this test modifies the global otel TextMapPropagator. + + // Set up a propagator to extract W3C Trace Context headers. + originalPropagator := otel.GetTextMapPropagator() + otel.SetTextMapPropagator(propagation.TraceContext{}) + t.Cleanup(func() { otel.SetTextMapPropagator(originalPropagator) }) + server, tracer, exporter := newTracingServer(t) httpsrv := httptest.NewServer(server) t.Cleanup(httpsrv.Close) + + // Define the expected trace and span IDs for context propagation. + const ( + traceID = "4bf92f3577b34da6a3ce929d0e0e4736" + parentSpanID = "00f067aa0ba902b7" + traceparent = "00-" + traceID + "-" + parentSpanID + "-01" + ) + client, err := DialHTTP(httpsrv.URL) if err != nil { t.Fatalf("failed to dial: %v", err) } t.Cleanup(client.Close) + // Set trace context headers. + client.SetHeader("traceparent", traceparent) + // Make a successful RPC call. var result echoResult if err := client.Call(&result, "test_echo", "hello", 42, &echoArgs{S: "world"}); err != nil { @@ -92,8 +111,10 @@ func TestTracingHTTP(t *testing.T) { } } if rpcSpan == nil { - t.Fatalf("jsonrpc.test/echo span not found.") + t.Fatalf("jsonrpc.test/echo span not found") } + + // Verify span attributes. attrs := attributeMap(rpcSpan.Attributes) if attrs["rpc.system"] != "jsonrpc" { t.Errorf("expected rpc.system=jsonrpc, got %v", attrs["rpc.system"]) @@ -107,6 +128,17 @@ func TestTracingHTTP(t *testing.T) { if _, ok := attrs["rpc.jsonrpc.request_id"]; !ok { t.Errorf("expected rpc.jsonrpc.request_id attribute to be set") } + + // Verify the span's parent matches the traceparent header values. + if got := rpcSpan.Parent.TraceID().String(); got != traceID { + t.Errorf("parent trace ID mismatch: got %s, want %s", got, traceID) + } + if got := rpcSpan.Parent.SpanID().String(); got != parentSpanID { + t.Errorf("parent span ID mismatch: got %s, want %s", got, parentSpanID) + } + if !rpcSpan.Parent.IsRemote() { + t.Error("expected parent span context to be marked as remote") + } } // TestTracingBatchHTTP verifies that RPC spans are emitted for batched JSON-RPC calls over HTTP.