diff --git a/rest/httpc/requests.go b/rest/httpc/requests.go index b6c9e861..b9c2d3bb 100644 --- a/rest/httpc/requests.go +++ b/rest/httpc/requests.go @@ -7,13 +7,20 @@ import ( "fmt" "io" "net/http" + "net/http/httptrace" nurl "net/url" "strings" "github.com/zeromicro/go-zero/core/lang" "github.com/zeromicro/go-zero/core/mapping" + "github.com/zeromicro/go-zero/core/trace" "github.com/zeromicro/go-zero/rest/httpc/internal" "github.com/zeromicro/go-zero/rest/internal/header" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + oteltrace "go.opentelemetry.io/otel/trace" ) var interceptors = []internal.Interceptor{ @@ -150,17 +157,47 @@ func fillPath(u *nurl.URL, val map[string]interface{}) error { } func request(r *http.Request, cli client) (*http.Response, error) { - var respHandlers []internal.ResponseHandler - for _, interceptor := range interceptors { + tracer := otel.GetTracerProvider().Tracer(trace.TraceName) + propagator := otel.GetTextMapPropagator() + + spanName := r.URL.Path + ctx, span := tracer.Start( + r.Context(), + spanName, + oteltrace.WithSpanKind(oteltrace.SpanKindClient), + oteltrace.WithAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...), + ) + defer span.End() + + respHandlers := make([]internal.ResponseHandler, len(interceptors)) + for i, interceptor := range interceptors { var h internal.ResponseHandler r, h = interceptor(r) - respHandlers = append(respHandlers, h) + respHandlers[i] = h } + clientTrace := httptrace.ContextClientTrace(ctx) + if clientTrace != nil { + ctx = httptrace.WithClientTrace(ctx, clientTrace) + } + + r = r.WithContext(ctx) + span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...) + propagator.Inject(ctx, propagation.HeaderCarrier(r.Header)) + resp, err := cli.do(r) for i := len(respHandlers) - 1; i >= 0; i-- { respHandlers[i](resp, err) } + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return resp, err + } + + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + span.SetStatus(semconv.SpanStatusFromHTTPStatusCode(resp.StatusCode)) + return resp, err } diff --git a/rest/httpc/requests_test.go b/rest/httpc/requests_test.go index c6160c48..e8edf2b1 100644 --- a/rest/httpc/requests_test.go +++ b/rest/httpc/requests_test.go @@ -4,15 +4,25 @@ import ( "context" "net/http" "net/http/httptest" + "net/http/httptrace" "testing" "github.com/stretchr/testify/assert" + ztrace "github.com/zeromicro/go-zero/core/trace" "github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/internal/header" "github.com/zeromicro/go-zero/rest/router" + "go.opentelemetry.io/otel/trace" ) func TestDoRequest(t *testing.T) { + ztrace.StartAgent(ztrace.Config{ + Name: "go-zero-test", + Endpoint: "http://localhost:14268/api/traces", + Batcher: "jaeger", + Sampler: 1.0, + }) + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer svr.Close() @@ -21,6 +31,8 @@ func TestDoRequest(t *testing.T) { resp, err := DoRequest(req) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) + spanContext := trace.SpanContextFromContext(resp.Request.Context()) + assert.True(t, spanContext.IsValid()) } func TestDoRequest_NotFound(t *testing.T) { @@ -187,3 +199,17 @@ func TestDo_Json(t *testing.T) { _, err = Do(context.Background(), http.MethodPost, svr.URL+"/nodes/:key", data) assert.NotNil(t, err) } + +func TestDo_WithClientHttpTrace(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer svr.Close() + + _, err := Do(httptrace.WithClientTrace(context.Background(), + &httptrace.ClientTrace{ + DNSStart: func(info httptrace.DNSStartInfo) { + assert.Equal(t, "localhost", info.Host) + }, + }), http.MethodGet, svr.URL, nil) + assert.Nil(t, err) + +}