mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-12 01:41:36 +00:00
rpc: add option to configure TextMapPropagator on client (#35132)
This adds a client option to configure trace context propagation via the `traceparent` HTTP header. I'm adding this so that prysm can enable distributed tracing on their engine API client.
This commit is contained in:
parent
39b17c5585
commit
e444c267a2
3 changed files with 94 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientOption is a configuration option for the RPC client.
|
// ClientOption is a configuration option for the RPC client.
|
||||||
|
|
@ -32,6 +33,7 @@ type clientConfig struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
httpHeaders http.Header
|
httpHeaders http.Header
|
||||||
httpAuth HTTPAuth
|
httpAuth HTTPAuth
|
||||||
|
tmprop propagation.TextMapPropagator
|
||||||
|
|
||||||
// WebSocket options
|
// WebSocket options
|
||||||
wsDialer *websocket.Dialer
|
wsDialer *websocket.Dialer
|
||||||
|
|
@ -95,6 +97,22 @@ func WithHeaders(headers http.Header) ClientOption {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTextMapPropagator configures OpenTelemetry trace propagation.
|
||||||
|
// Note, by default, trace context is NOT propagated by rpc.Client.
|
||||||
|
// To enable propagation via the `traceparent` header, you must explicitly
|
||||||
|
// enable it by setting a propagator, e.g.
|
||||||
|
//
|
||||||
|
// prop := propagation.TraceContext{}
|
||||||
|
// c, err := rpc.DialOptions(ctx, "http://", rpc.WithTextMapPropagator(prop))
|
||||||
|
func WithTextMapPropagator(tmp propagation.TextMapPropagator) ClientOption {
|
||||||
|
if tmp == nil {
|
||||||
|
panic("nil TextMapPropagator configured")
|
||||||
|
}
|
||||||
|
return optionFunc(func(cfg *clientConfig) {
|
||||||
|
cfg.tmprop = tmp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WithHTTPClient configures the http.Client used by the RPC client.
|
// WithHTTPClient configures the http.Client used by the RPC client.
|
||||||
func WithHTTPClient(c *http.Client) ClientOption {
|
func WithHTTPClient(c *http.Client) ClientOption {
|
||||||
return optionFunc(func(cfg *clientConfig) {
|
return optionFunc(func(cfg *clientConfig) {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ type httpConn struct {
|
||||||
mu sync.Mutex // protects headers
|
mu sync.Mutex // protects headers
|
||||||
headers http.Header
|
headers http.Header
|
||||||
auth HTTPAuth
|
auth HTTPAuth
|
||||||
|
tmprop propagation.TextMapPropagator
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpConn implements ServerCodec, but it is treated specially by Client
|
// httpConn implements ServerCodec, but it is treated specially by Client
|
||||||
|
|
@ -168,6 +169,7 @@ func newClientTransportHTTP(endpoint string, cfg *clientConfig) reconnectFunc {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
url: endpoint,
|
url: endpoint,
|
||||||
auth: cfg.httpAuth,
|
auth: cfg.httpAuth,
|
||||||
|
tmprop: cfg.tmprop,
|
||||||
closeCh: make(chan interface{}),
|
closeCh: make(chan interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,6 +232,9 @@ func (hc *httpConn) doRequest(ctx context.Context, body []byte) (io.ReadCloser,
|
||||||
req.Header = hc.headers.Clone()
|
req.Header = hc.headers.Clone()
|
||||||
hc.mu.Unlock()
|
hc.mu.Unlock()
|
||||||
setHeaders(req.Header, headersFromContext(ctx))
|
setHeaders(req.Header, headersFromContext(ctx))
|
||||||
|
if hc.tmprop != nil {
|
||||||
|
hc.tmprop.Inject(ctx, propagation.HeaderCarrier(req.Header))
|
||||||
|
}
|
||||||
|
|
||||||
if hc.auth != nil {
|
if hc.auth != nil {
|
||||||
if err := hc.auth(req.Header); err != nil {
|
if err := hc.auth(req.Header); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -541,6 +542,76 @@ func TestTracingBatchHTTPTooLarge(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newHeaderRecordingServer creates an HTTP test server that responds to any
|
||||||
|
// JSON-RPC call and records the traceparent header of incoming requests.
|
||||||
|
func newHeaderRecordingServer(t *testing.T, headerCh chan<- string) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
headerCh <- r.Header.Get("traceparent")
|
||||||
|
var msg jsonrpcMessage
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
t.Errorf("invalid request body: %v", err)
|
||||||
|
}
|
||||||
|
resp := jsonrpcMessage{Version: vsn, ID: msg.ID, Result: []byte("null")}
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
json.NewEncoder(w).Encode(&resp)
|
||||||
|
}))
|
||||||
|
t.Cleanup(httpsrv.Close)
|
||||||
|
return httpsrv
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTracingClientPropagation verifies that the client injects the W3C
|
||||||
|
// traceparent header into outgoing HTTP requests when configured with the
|
||||||
|
// WithTextMapPropagator option.
|
||||||
|
func TestTracingClientPropagation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
headerCh := make(chan string, 1)
|
||||||
|
httpsrv := newHeaderRecordingServer(t, headerCh)
|
||||||
|
|
||||||
|
client, err := DialOptions(context.Background(), httpsrv.URL, WithTextMapPropagator(propagation.TraceContext{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(client.Close)
|
||||||
|
|
||||||
|
// Build a context carrying a sampled remote span context.
|
||||||
|
const (
|
||||||
|
traceID = "4bf92f3577b34da6a3ce929d0e0e4736"
|
||||||
|
spanID = "00f067aa0ba902b7"
|
||||||
|
)
|
||||||
|
tid, err := trace.TraceIDFromHex(traceID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sid, err := trace.SpanIDFromHex(spanID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sc := trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: tid,
|
||||||
|
SpanID: sid,
|
||||||
|
TraceFlags: trace.FlagsSampled,
|
||||||
|
})
|
||||||
|
ctx := trace.ContextWithSpanContext(context.Background(), sc)
|
||||||
|
|
||||||
|
if err := client.CallContext(ctx, nil, "test_foo"); err != nil {
|
||||||
|
t.Fatalf("RPC call failed: %v", err)
|
||||||
|
}
|
||||||
|
want := "00-" + traceID + "-" + spanID + "-01"
|
||||||
|
if got := <-headerCh; got != want {
|
||||||
|
t.Errorf("traceparent header: got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A call without a span context in ctx must not produce a traceparent header.
|
||||||
|
if err := client.CallContext(context.Background(), nil, "test_foo"); err != nil {
|
||||||
|
t.Fatalf("RPC call failed: %v", err)
|
||||||
|
}
|
||||||
|
if got := <-headerCh; got != "" {
|
||||||
|
t.Errorf("traceparent header without span context: got %q, want none", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestTracingHTTPTimeout verifies that when a non-batch call exceeds the HTTP
|
// TestTracingHTTPTimeout verifies that when a non-batch call exceeds the HTTP
|
||||||
// server's WriteTimeout, the SERVER span ends with error status (carrying the
|
// server's WriteTimeout, the SERVER span ends with error status (carrying the
|
||||||
// timeout error message).
|
// timeout error message).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue