mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-21 07:19:28 +00:00
rpc: extract OpenTelemetry trace context from request headers (#33599)
This PR adds support for the extraction of OpenTelemetry trace context from incoming JSON-RPC request headers, allowing geth spans to be linked to upstream traces when present. --------- Co-authored-by: lightclient <lightclient@protonmail.com>
This commit is contained in:
parent
a9acb3ff93
commit
e3e556b266
2 changed files with 40 additions and 2 deletions
|
|
@ -30,6 +30,9 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -334,6 +337,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo)
|
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
|
// 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
|
// until EOF, writes the response to w, and orders the server to process a
|
||||||
// single request.
|
// single request.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
"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.
|
// TestTracingHTTP verifies that RPC spans are emitted when processing HTTP requests.
|
||||||
func TestTracingHTTP(t *testing.T) {
|
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)
|
server, tracer, exporter := newTracingServer(t)
|
||||||
httpsrv := httptest.NewServer(server)
|
httpsrv := httptest.NewServer(server)
|
||||||
t.Cleanup(httpsrv.Close)
|
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)
|
client, err := DialHTTP(httpsrv.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to dial: %v", err)
|
t.Fatalf("failed to dial: %v", err)
|
||||||
}
|
}
|
||||||
t.Cleanup(client.Close)
|
t.Cleanup(client.Close)
|
||||||
|
|
||||||
|
// Set trace context headers.
|
||||||
|
client.SetHeader("traceparent", traceparent)
|
||||||
|
|
||||||
// Make a successful RPC call.
|
// Make a successful RPC call.
|
||||||
var result echoResult
|
var result echoResult
|
||||||
if err := client.Call(&result, "test_echo", "hello", 42, &echoArgs{S: "world"}); err != nil {
|
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 {
|
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)
|
attrs := attributeMap(rpcSpan.Attributes)
|
||||||
if attrs["rpc.system"] != "jsonrpc" {
|
if attrs["rpc.system"] != "jsonrpc" {
|
||||||
t.Errorf("expected rpc.system=jsonrpc, got %v", attrs["rpc.system"])
|
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 {
|
if _, ok := attrs["rpc.jsonrpc.request_id"]; !ok {
|
||||||
t.Errorf("expected rpc.jsonrpc.request_id attribute to be set")
|
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.
|
// TestTracingBatchHTTP verifies that RPC spans are emitted for batched JSON-RPC calls over HTTP.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue