diff --git a/README.md b/README.md index 82a9742cd..0fdb85497 100644 --- a/README.md +++ b/README.md @@ -121,18 +121,11 @@ import ( ot "github.com/opentracing/opentracing-go" ) -// Doing registration and wrapping in two separate steps +// Doing registration and wrapping func main() { http.HandleFunc( "/path/to/handler", - sensor.TracingHandler("myHandler", myHandler), - ) -} - -// Doing registration and wrapping in a single step -func main() { - http.HandleFunc( - sensor.TraceHandler("myHandler", "/path/to/handler", myHandler), + sensor.TracingHandlerFunc("myHandler", myHandler), ) } @@ -151,23 +144,20 @@ func myHandler(w http.ResponseWriter, req *http.Request) { Requesting data or information from other, often external systems, is commonly implemented through HTTP requests. To make sure traces contain all spans, especially over all the different systems, certain span information have to be injected into the HTTP request headers before sending it out. Instana's Go sensor provides support to automate this process as much as possible. -To have Instana inject information into the request headers, create the `http.Request` as normal and wrap it with the Instana sensor function as in the following example. +To have Instana inject information into the request headers, create the `http.Client`, wrap its `Transport` with `instana.RoundTripper()` and use it as in the following example. ```go req, err := http.NewRequest("GET", url, nil) -client := &http.Client{} -resp, err := sensor.TracingHttpRequest( - "myExternalCall", - parentSpan, - req, - client, -) +client := &http.Client{ + Transport: instana.RoundTripper(sensor, nil), +} + +ctx := instana.ContextWithSpan(context.Background(), parentSpan) +resp, err := client.Do(req.WithContext(ctx)) ``` The provided `parentSpan` is the incoming request from the request handler (see above) and provides the necessary tracing and span information to create a child span and inject it into the request. -The request is, after injection, executing using the provided `http.Client` instance. Like the normal `(*http.Client).Do()` operation, the call will return a `http.Response` instance or an error proving information of the failure reason. - ### GRPC servers and clients [`github.com/instana/go-sensor/instrumentation/instagrpc`](./instrumentation/instagrpc) provides both unary and stream interceptors to instrument GRPC servers and clients that use `google.golang.org/grpc`. diff --git a/adapters.go b/adapters.go index b678b0e79..29e97cf51 100644 --- a/adapters.go +++ b/adapters.go @@ -54,63 +54,34 @@ func (s *Sensor) SetLogger(l LeveledLogger) { // TraceHandler is similar to TracingHandler in regards, that it wraps an existing http.HandlerFunc // into a named instance to support capturing tracing information and data. The returned values are // compatible with handler registration methods, e.g. http.Handle() +// +// Deprecated: please use instana.TracingHandlerFunc() instead func (s *Sensor) TraceHandler(name, pattern string, handler http.HandlerFunc) (string, http.HandlerFunc) { return pattern, s.TracingHandler(name, handler) } // TracingHandler wraps an existing http.HandlerFunc into a named instance to support capturing tracing // information and response data +// +// Deprecated: please use instana.TracingHandlerFunc() instead func (s *Sensor) TracingHandler(name string, handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - s.WithTracingContext(name, w, req, func(span ot.Span, ctx context.Context) { - wrapped := &statusCodeRecorder{ResponseWriter: w} - handler.ServeHTTP(wrapped, req.WithContext(ctx)) - - if wrapped.Status > 0 { - span.SetTag(string(ext.HTTPStatusCode), wrapped.Status) - } - }) - } + return TracingHandlerFunc(s, name, handler) } // TracingHttpRequest wraps an existing http.Request instance into a named instance to inject tracing and span // header information into the actual HTTP wire transfer +// +// Deprecated: please use instana.RoundTripper() instead func (s *Sensor) TracingHttpRequest(name string, parent, req *http.Request, client http.Client) (*http.Response, error) { - opts := []ot.StartSpanOption{ - ext.SpanKindRPCClient, - ot.Tags{ - string(ext.PeerHostname): req.Host, - string(ext.HTTPUrl): req.URL.String(), - string(ext.HTTPMethod): req.Method, - }, - } - - if parentSpan, ok := SpanFromContext(parent.Context()); ok { - opts = append(opts, ot.ChildOf(parentSpan.Context())) - } - - span := s.tracer.StartSpan("client", opts...) - defer span.Finish() - - headersCarrier := ot.HTTPHeadersCarrier(req.Header) - if err := s.tracer.Inject(span.Context(), ot.HTTPHeaders, headersCarrier); err != nil { - return nil, err - } - - res, err := client.Do(req.WithContext(context.Background())) - if err != nil { - span.LogFields(otlog.Error(err)) - return res, err - } - - span.SetTag(string(ext.HTTPStatusCode), res.StatusCode) - - return res, nil + client.Transport = RoundTripper(s, client.Transport) + return client.Do(req.WithContext(context.Background())) } // WithTracingSpan takes the given SpanSensitiveFunc and executes it under the scope of a child span, which is // injected as an argument when calling the function. It uses the name of the caller as a span operation name // unless a non-empty value is provided +// +// Deprecated: please use instana.TracingHandlerFunc() to instrument an HTTP handler func (s *Sensor) WithTracingSpan(operationName string, w http.ResponseWriter, req *http.Request, f SpanSensitiveFunc) { if operationName == "" { pc, _, _, _ := runtime.Caller(1) @@ -169,27 +140,10 @@ func (s *Sensor) WithTracingSpan(operationName string, w http.ResponseWriter, re // Executes the given ContextSensitiveFunc and executes it under the scope of a newly created context.Context, // that provides access to the parent span as 'parentSpan'. +// +// Deprecated: please use instana.TracingHandlerFunc() to instrument an HTTP handler func (s *Sensor) WithTracingContext(name string, w http.ResponseWriter, req *http.Request, f ContextSensitiveFunc) { s.WithTracingSpan(name, w, req, func(span ot.Span) { f(span, ContextWithSpan(req.Context(), span)) }) } - -// wrapper over http.ResponseWriter to spy the returned status code -type statusCodeRecorder struct { - http.ResponseWriter - Status int -} - -func (rec *statusCodeRecorder) WriteHeader(status int) { - rec.Status = status - rec.ResponseWriter.WriteHeader(status) -} - -func (rec *statusCodeRecorder) Write(b []byte) (int, error) { - if rec.Status == 0 { - rec.Status = http.StatusOK - } - - return rec.ResponseWriter.Write(b) -} diff --git a/adapters_test.go b/adapters_test.go index 084f52179..20b170a34 100644 --- a/adapters_test.go +++ b/adapters_test.go @@ -116,7 +116,7 @@ func TestTracingHttpRequest(t *testing.T) { require.NotNil(t, span.Data) require.NotNil(t, span.Data.SDK) - assert.Equal(t, "client", span.Data.SDK.Name) + assert.Equal(t, "net/http.Client", span.Data.SDK.Name) assert.Equal(t, "exit", span.Data.SDK.Type) require.NotNil(t, span.Data.SDK.Custom) diff --git a/example/README.md b/example/README.md new file mode 100644 index 000000000..9dcbc044a --- /dev/null +++ b/example/README.md @@ -0,0 +1,7 @@ +Examples +======== + +For up-to-date instrumentation code examples please consult the respective godoc: + +* [`github.com/instana/go-sensor`](https://pkg.go.dev/github.com/instana/go-sensor?tab=doc#pkg-overview) - HTTP client and server instrumentation +* [`github.com/instana/go-sensor/instrumentation/instagrpc`](https://pkg.go.dev/github.com/instana/go-sensor/instrumentation/instagrpc?tab=doc#pkg-overview) - GRPC server and client instrumentation diff --git a/example/httpclient/multi_request.go b/example/httpclient/multi_request.go deleted file mode 100644 index 18fb87685..000000000 --- a/example/httpclient/multi_request.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "time" - - instana "github.com/instana/go-sensor" - ot "github.com/opentracing/opentracing-go" -) - -func main() { - opts := instana.Options{LogLevel: instana.Debug} - recorder := instana.NewRecorder() - tracer := instana.NewTracerWithEverything(&opts, recorder) - - fmt.Println("Hello. Sleeping for 10 to allow announce.") - time.Sleep(10 * time.Second) - - client := &http.Client{} - req, err := http.NewRequest("GET", "http://gameface.in/", nil) - - if err != nil { - fmt.Println(err) - } - - for i := 1; i <= 100; i++ { - sp := tracer.StartSpan("multi_request") - sp.SetBaggageItem("foo", "bar") - for i := 1; i <= 2; i++ { - - headersCarrier := ot.HTTPHeadersCarrier(req.Header) - if err = tracer.Inject(sp.Context(), ot.HTTPHeaders, headersCarrier); err != nil { - fmt.Println(err) - } - - httpSpan := tracer.StartSpan("net-http", ot.ChildOf(sp.Context())) - fmt.Println("Making request to Gameface...") - resp, err := client.Do(req) - - if err != nil { - fmt.Println(err) - } else { - httpSpan.SetTag("http.status_code", resp.StatusCode) - } - - fmt.Println("Done. Code & sleeping for 5. StatusCode: ", resp.StatusCode) - time.Sleep(5 * time.Second) - - httpSpan.Finish() - } - sp.Finish() - } -} diff --git a/example/webserver/instana/http.go b/example/webserver/instana/http.go deleted file mode 100644 index 57a31c601..000000000 --- a/example/webserver/instana/http.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "net/http" - "time" - - instana "github.com/instana/go-sensor" -) - -const ( - Service = "go-microservice-14c" - Entry = "http://localhost:9060/golang/entry" - Exit1 = "http://localhost:9060/golang/exit" - Exit2 = "http://localhost:9060/instana/exit" -) - -var sensor = instana.NewSensor(Service) - -func request(url string) *http.Request { - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Content-Type", "text/plain") - return req -} - -func requestEntry() { - client := &http.Client{Timeout: 5 * time.Second} - req := request(Entry) - client.Do(req) -} - -func requestExit1(parent *http.Request) (*http.Response, error) { - client := http.Client{Timeout: 5 * time.Second} - req := request(Exit1) - return sensor.TracingHttpRequest("exit", parent, req, client) -} - -func requestExit2(parent *http.Request) (*http.Response, error) { - client := http.Client{Timeout: 5 * time.Second} - req := request(Exit2) - return sensor.TracingHttpRequest("exit", parent, req, client) -} - -func server() { - // Wrap and register in one shot - http.HandleFunc( - sensor.TraceHandler("entry-handler", "/golang/entry", - func(writer http.ResponseWriter, req *http.Request) { - requestExit1(req) - time.Sleep(time.Second) - requestExit2(req) - }, - ), - ) - - // Wrap and register in two separate steps, depending on your preference - http.HandleFunc("/golang/exit", - sensor.TracingHandler("exit-handler", func(w http.ResponseWriter, req *http.Request) { - time.Sleep(450 * time.Millisecond) - }), - ) - - // Wrap and register in two separate steps, depending on your preference - http.HandleFunc("/instana/exit", - sensor.TracingHandler("exit-handler", func(w http.ResponseWriter, req *http.Request) { - time.Sleep(450 * time.Millisecond) - }), - ) - - if err := http.ListenAndServe(":9060", nil); err != nil { - panic(err) - } -} - -func main() { - go server() - go forever() - select {} -} - -func forever() { - for { - requestEntry() - time.Sleep(500 * time.Millisecond) - } -} diff --git a/example/webserver/opentracing/http.go b/example/webserver/opentracing/http.go deleted file mode 100644 index e18c30c46..000000000 --- a/example/webserver/opentracing/http.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "log" - "net/http" - "time" - - instana "github.com/instana/go-sensor" - ot "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - "golang.org/x/net/context" -) - -const ( - Service = "go-microservice-14c" - Entry = "http://localhost:9060/golang/entry" - Exit = "http://localhost:9060/golang/exit" -) - -func request(ctx context.Context, url string, op string) (*http.Client, *http.Request) { - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Content-Type", "text/plain") - client := &http.Client{Timeout: 5 * time.Second} - - return client, req -} - -func requestEntry(ctx context.Context) { - client, req := request(ctx, Entry, "entry") - client.Do(req) -} - -//TODO: handle erroneous requests -func requestExit(span ot.Span) { - client, req := request(context.Background(), Exit, "exit") - ot.GlobalTracer().Inject(span.Context(), ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header)) - resp, _ := client.Do(req) - span.SetTag(string(ext.SpanKind), string(ext.SpanKindRPCClientEnum)) - span.SetTag(string(ext.PeerHostname), req.Host) - span.SetTag(string(ext.HTTPUrl), Exit) - span.SetTag(string(ext.HTTPMethod), req.Method) - span.SetTag(string(ext.HTTPStatusCode), resp.StatusCode) -} - -func server() { - http.HandleFunc("/golang/entry", func(w http.ResponseWriter, req *http.Request) { - wireContext, _ := ot.GlobalTracer().Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header)) - parentSpan := ot.GlobalTracer().StartSpan("server", ext.RPCServerOption(wireContext)) - parentSpan.SetTag(string(ext.SpanKind), string(ext.SpanKindRPCServerEnum)) - parentSpan.SetTag(string(ext.PeerHostname), req.Host) - parentSpan.SetTag(string(ext.HTTPUrl), req.URL.Path) - parentSpan.SetTag(string(ext.HTTPMethod), req.Method) - parentSpan.SetTag(string(ext.HTTPStatusCode), 200) - - childSpan := ot.StartSpan("client", ot.ChildOf(parentSpan.Context())) - - requestExit(childSpan) - - time.Sleep(450 * time.Millisecond) - - childSpan.Finish() - - time.Sleep(550 * time.Millisecond) - - ot.GlobalTracer().Inject(parentSpan.Context(), ot.HTTPHeaders, ot.HTTPHeadersCarrier(w.Header())) - - parentSpan.Finish() - }) - - http.HandleFunc("/golang/exit", func(w http.ResponseWriter, req *http.Request) { - time.Sleep(450 * time.Millisecond) - }) - - log.Fatal(http.ListenAndServe(":9060", nil)) -} - -func main() { - ot.InitGlobalTracer(instana.NewTracerWithOptions(&instana.Options{ - Service: Service, - LogLevel: instana.Info})) - - go server() - - go forever() - select {} -} - -func forever() { - for { - requestEntry(context.Background()) - } -} diff --git a/example_httpclient_test.go b/example_httpclient_test.go new file mode 100644 index 000000000..70028a0ac --- /dev/null +++ b/example_httpclient_test.go @@ -0,0 +1,42 @@ +package instana_test + +import ( + "context" + "log" + "net/http" + + instana "github.com/instana/go-sensor" +) + +// This example shows how to instrument an HTTP client with Instana tracing +func Example_roundTripper() { + sensor := instana.NewSensor("my-http-client") + + // Wrap the original http.Client transport with instana.RoundTripper(). + // The http.DefaultTransport will be used if there was no transport provided. + client := &http.Client{ + Transport: instana.RoundTripper(sensor, nil), + } + + // Use your instrumented http.Client to propagate tracing context with the request + _, err := client.Get("https://www.instana.com") + if err != nil { + log.Fatalf("failed to GET https://www.instana.com: %s", err) + } + + // To propagate the existing trace with request, make sure that current span is added + // to the request context first. + span := sensor.Tracer().StartSpan("query-instana") + defer span.Finish() + + ctx := instana.ContextWithSpan(context.Background(), span) + req, err := http.NewRequest("GET", "https://www.instana.com", nil) + if err != nil { + log.Fatalf("failed to create a new request: %s", err) + } + + _, err = client.Do(req.WithContext(ctx)) + if err != nil { + log.Fatalf("failed to GET https://www.instana.com: %s", err) + } +} diff --git a/example_httpserver_test.go b/example_httpserver_test.go new file mode 100644 index 000000000..641fbc5ac --- /dev/null +++ b/example_httpserver_test.go @@ -0,0 +1,34 @@ +package instana_test + +import ( + "log" + "net/http" + + instana "github.com/instana/go-sensor" +) + +// This example shows how to instrument an HTTP server with Instana tracing +func Example_tracingHandlerFunc() { + sensor := instana.NewSensor("my-http-server") + + // To instrument a handler function, pass it as an argument to instana.TracingHandlerFunc() + http.HandleFunc("/", instana.TracingHandlerFunc(sensor, "/", func(w http.ResponseWriter, req *http.Request) { + // Extract the parent span and use its tracer to initialize any child spans to trace the calls + // inside the handler, e.g. database queries, 3rd-party API requests, etc. + if parent, ok := instana.SpanFromContext(req.Context()); ok { + sp := parent.Tracer().StartSpan("index") + defer sp.Finish() + } + + // ... + + w.Write([]byte("OK")) + })) + + // In case your handler is implemented as an http.Handler, pass its ServeHTTP method instead + http.HandleFunc("/files", instana.TracingHandlerFunc(sensor, "index", http.FileServer(http.Dir("./")).ServeHTTP)) + + if err := http.ListenAndServe(":0", nil); err != nil { + log.Fatalf("failed to start server: %s", err) + } +} diff --git a/example_instrumentation_test.go b/example_instrumentation_test.go new file mode 100644 index 000000000..8e1469415 --- /dev/null +++ b/example_instrumentation_test.go @@ -0,0 +1,36 @@ +package instana_test + +import ( + "net/http" + + instana "github.com/instana/go-sensor" +) + +// This example demonstrates how to instrument an HTTP handler with Instana and register it +// in http.DefaultServeMux +func ExampleTracingHandlerFunc() { + // Here we initialize a new instance of instana.Sensor, however it is STRONGLY recommended + // to use a single instance throughout your application + sensor := instana.NewSensor("my-http-server") + + http.HandleFunc("/", instana.TracingHandlerFunc(sensor, "root", func(w http.ResponseWriter, req *http.Request) { + // handler code + })) +} + +// This example demonstrates how to instrument an HTTP client with Instana +func ExampleRoundTripper() { + // Here we initialize a new instance of instana.Sensor, however it is STRONGLY recommended + // to use a single instance throughout your application + sensor := instana.NewSensor("my-http-client") + + // http.DefaultTransport is used as a default RoundTripper, however you can provide + // your own implementation + client := &http.Client{ + Transport: instana.RoundTripper(sensor, nil), + } + + // Execute request as usual + req, _ := http.NewRequest("GET", "https://www.instana.com", nil) + client.Do(req) +} diff --git a/instrumentation.go b/instrumentation.go new file mode 100644 index 000000000..90394cec9 --- /dev/null +++ b/instrumentation.go @@ -0,0 +1,280 @@ +package instana + +import ( + "context" + "mime/multipart" + "net/http" + "net/textproto" + "net/url" + + ot "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + otlog "github.com/opentracing/opentracing-go/log" +) + +// TracingHandlerFunc is an HTTP middleware that captures the tracing data and ensures +// trace context propagation via OpenTracing headers +func TracingHandlerFunc(sensor *Sensor, name string, handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + opts := []ot.StartSpanOption{ + ext.SpanKindRPCServer, + ot.Tags{ + string(ext.PeerHostname): req.Host, + string(ext.HTTPUrl): req.URL.Path, + string(ext.HTTPMethod): req.Method, + }, + } + + tracer := sensor.Tracer() + if ps, ok := SpanFromContext(req.Context()); ok { + tracer = ps.Tracer() + opts = append(opts, ot.ChildOf(ps.Context())) + } + + wireContext, err := tracer.Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header)) + switch err { + case nil: + opts = append(opts, ext.RPCServerOption(wireContext)) + case ot.ErrSpanContextNotFound: + sensor.Logger().Debug("no span context provided with", req.Method, req.URL.Path) + case ot.ErrUnsupportedFormat: + sensor.Logger().Info("unsupported span context format provided with", req.Method, req.URL.Path) + default: + sensor.Logger().Warn("failed to extract span context from the request:", err) + } + + span := tracer.StartSpan(name, opts...) + defer span.Finish() + + defer func() { + // Be sure to capture any kind of panic / error + if err := recover(); err != nil { + if e, ok := err.(error); ok { + span.SetTag("message", e.Error()) + span.SetTag("http.error", e.Error()) + span.LogFields(otlog.Error(e)) + } else { + span.SetTag("message", err) + span.SetTag("http.error", err) + span.LogFields(otlog.Object("error", err)) + } + + span.SetTag(string(ext.HTTPStatusCode), http.StatusInternalServerError) + + // re-throw the panic + panic(err) + } + }() + + wrapped := &statusCodeRecorder{ResponseWriter: w} + tracer.Inject(span.Context(), ot.HTTPHeaders, ot.HTTPHeadersCarrier(wrapped.Header())) + + ctx = ContextWithSpan(ctx, span) + handler(wrapped, req.WithContext(ctx)) + + if wrapped.Status > 0 { + span.SetTag(string(ext.HTTPStatusCode), wrapped.Status) + } + } +} + +// RoundTripper wraps an existing http.RoundTripper and injects the tracing headers into the outgoing request. +// If the original RoundTripper is nil, the http.DefaultTransport will be used. +func RoundTripper(sensor *Sensor, original http.RoundTripper) http.RoundTripper { + return tracingRoundTripper(func(req *http.Request) (*http.Response, error) { + ctx := req.Context() + + opts := []ot.StartSpanOption{ + ext.SpanKindRPCClient, + ot.Tags{ + string(ext.PeerHostname): req.Host, + string(ext.HTTPUrl): req.URL.String(), + string(ext.HTTPMethod): req.Method, + }, + } + + tracer := sensor.Tracer() + // use the parent span tracer and context if provided + if ps, ok := SpanFromContext(ctx); ok { + tracer = ps.Tracer() + opts = append(opts, ot.ChildOf(ps.Context())) + } + + span := tracer.StartSpan("net/http.Client", opts...) + defer span.Finish() + + // clone the request since the RoundTrip should not modify the original one + req = cloneRequest(ContextWithSpan(ctx, span), req) + tracer.Inject(span.Context(), ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header)) + + if original == nil { + original = http.DefaultTransport + } + + resp, err := original.RoundTrip(req) + if err != nil { + span.SetTag("message", err.Error()) + span.SetTag("http.error", err.Error()) + span.LogFields(otlog.Error(err)) + return resp, err + } + + span.SetTag(string(ext.HTTPStatusCode), resp.StatusCode) + + return resp, err + }) +} + +// wrapper over http.ResponseWriter to spy the returned status code +type statusCodeRecorder struct { + http.ResponseWriter + Status int +} + +func (rec *statusCodeRecorder) WriteHeader(status int) { + rec.Status = status + rec.ResponseWriter.WriteHeader(status) +} + +func (rec *statusCodeRecorder) Write(b []byte) (int, error) { + if rec.Status == 0 { + rec.Status = http.StatusOK + } + + return rec.ResponseWriter.Write(b) +} + +type tracingRoundTripper func(*http.Request) (*http.Response, error) + +func (rt tracingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt(req) +} + +// The following code is ported from $GOROOT/src/net/http/clone.go with minor changes +// for compatibility with Go versions prior to 1.13 +// +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +func cloneRequest(ctx context.Context, r *http.Request) *http.Request { + r2 := new(http.Request) + *r2 = *r + r2 = r2.WithContext(ctx) + + r2.URL = cloneURL(r.URL) + if r.Header != nil { + r2.Header = cloneHeader(r.Header) + } + + if r.Trailer != nil { + r2.Trailer = cloneHeader(r.Trailer) + } + + if s := r.TransferEncoding; s != nil { + s2 := make([]string, len(s)) + copy(s2, s) + r2.TransferEncoding = s + } + + r2.Form = cloneURLValues(r.Form) + r2.PostForm = cloneURLValues(r.PostForm) + r2.MultipartForm = cloneMultipartForm(r.MultipartForm) + + return r2 +} + +func cloneURLValues(v url.Values) url.Values { + if v == nil { + return nil + } + + // http.Header and url.Values have the same representation, so temporarily + // treat it like http.Header, which does have a clone: + + return url.Values(cloneHeader(http.Header(v))) +} + +func cloneURL(u *url.URL) *url.URL { + if u == nil { + return nil + } + + u2 := new(url.URL) + *u2 = *u + + if u.User != nil { + u2.User = new(url.Userinfo) + *u2.User = *u.User + } + + return u2 +} + +func cloneMultipartForm(f *multipart.Form) *multipart.Form { + if f == nil { + return nil + } + + f2 := &multipart.Form{ + Value: (map[string][]string)(cloneHeader(http.Header(f.Value))), + } + + if f.File != nil { + m := make(map[string][]*multipart.FileHeader) + for k, vv := range f.File { + vv2 := make([]*multipart.FileHeader, len(vv)) + for i, v := range vv { + vv2[i] = cloneMultipartFileHeader(v) + } + m[k] = vv2 + + } + + f2.File = m + } + + return f2 +} + +func cloneMultipartFileHeader(fh *multipart.FileHeader) *multipart.FileHeader { + if fh == nil { + return nil + } + + fh2 := new(multipart.FileHeader) + *fh2 = *fh + + fh2.Header = textproto.MIMEHeader(cloneHeader(http.Header(fh.Header))) + + return fh2 +} + +// The following code is ported from $GOROOT/src/net/http/header.go with minor changes +// for compatibility with Go versions prior to 1.13 +// +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +func cloneHeader(h http.Header) http.Header { + if h == nil { + return nil + } + + // Find total number of values. + nv := 0 + for _, vv := range h { + nv += len(vv) + } + sv := make([]string, nv) // shared backing array for headers' values + h2 := make(http.Header, len(h)) + for k, vv := range h { + n := copy(sv, vv) + h2[k] = sv[:n:n] + sv = sv[n:] + } + return h2 +} diff --git a/instrumentation_test.go b/instrumentation_test.go new file mode 100644 index 000000000..1dd86e8a6 --- /dev/null +++ b/instrumentation_test.go @@ -0,0 +1,322 @@ +package instana_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + instana "github.com/instana/go-sensor" + ot "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTracingHandlerFunc_Write(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{ + Service: "go-sensor-test", + }, recorder)) + + h := instana.TracingHandlerFunc(s, "test-handler", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintln(w, "Ok") + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/test?q=classified", nil)) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "Ok\n", rec.Body.String()) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.False(t, span.Error) + assert.Equal(t, 0, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "test-handler", span.Data.SDK.Name) + assert.Equal(t, "entry", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "http.status_code": http.StatusOK, + "http.method": "GET", + "http.url": "/test", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCServerEnum, + }, span.Data.SDK.Custom.Tags) + + // check whether the trace context has been sent back to the client + traceID, err := instana.Header2ID(rec.Header().Get(instana.FieldT)) + require.NoError(t, err) + assert.Equal(t, span.TraceID, traceID) + + spanID, err := instana.Header2ID(rec.Header().Get(instana.FieldS)) + require.NoError(t, err) + assert.Equal(t, span.SpanID, spanID) +} + +func TestTracingHandlerFunc_WriteHeaders(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + h := instana.TracingHandlerFunc(s, "test-handler", func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/test?q=classified", nil)) + + assert.Equal(t, http.StatusNotImplemented, rec.Code) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.False(t, span.Error) + assert.Equal(t, 0, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "test-handler", span.Data.SDK.Name) + assert.Equal(t, "entry", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "http.method": "GET", + "http.status_code": http.StatusNotImplemented, + "http.url": "/test", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCServerEnum, + }, span.Data.SDK.Custom.Tags) +} + +func TestTracingHandlerFunc_PanicHandling(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + h := instana.TracingHandlerFunc(s, "test-handler", func(w http.ResponseWriter, req *http.Request) { + panic("something went wrong") + }) + + rec := httptest.NewRecorder() + assert.Panics(t, func() { + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/test?q=classified", nil)) + }) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.True(t, span.Error) + assert.Equal(t, 1, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "test-handler", span.Data.SDK.Name) + assert.Equal(t, "entry", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "message": "something went wrong", + "http.error": "something went wrong", + "http.method": "GET", + "http.status_code": http.StatusInternalServerError, + "http.url": "/test", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCServerEnum, + }, span.Data.SDK.Custom.Tags) + + var logRecords []map[string]interface{} + for _, v := range span.Data.SDK.Custom.Logs { + logRecords = append(logRecords, v) + } + + require.Len(t, logRecords, 1) + assert.Equal(t, "something went wrong", logRecords[0]["error"]) +} + +func TestRoundTripper(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + rt := instana.RoundTripper(s, testRoundTripper(func(req *http.Request) (*http.Response, error) { + assert.NotEmpty(t, req.Header.Get(instana.FieldT)) + assert.NotEmpty(t, req.Header.Get(instana.FieldS)) + + return &http.Response{ + Status: http.StatusText(http.StatusNotImplemented), + StatusCode: http.StatusNotImplemented, + }, nil + })) + + resp, err := rt.RoundTrip(httptest.NewRequest("GET", "http://example.com/hello", nil)) + require.NoError(t, err) + assert.Equal(t, http.StatusNotImplemented, resp.StatusCode) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.False(t, span.Error) + assert.Equal(t, 0, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "net/http.Client", span.Data.SDK.Name) + assert.Equal(t, "exit", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "http.method": "GET", + "http.status_code": http.StatusNotImplemented, + "http.url": "http://example.com/hello", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCClientEnum, + }, span.Data.SDK.Custom.Tags) +} + +func TestRoundTripper_WithParentSpan(t *testing.T) { + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + s := instana.NewSensorWithTracer(tracer) + + span := tracer.StartSpan("parent") + + var traceIDHeader, spanIDHeader string + rt := instana.RoundTripper(s, testRoundTripper(func(req *http.Request) (*http.Response, error) { + traceIDHeader = req.Header.Get(instana.FieldT) + spanIDHeader = req.Header.Get(instana.FieldS) + + return &http.Response{ + Status: http.StatusText(http.StatusNotImplemented), + StatusCode: http.StatusNotImplemented, + }, nil + })) + + ctx := instana.ContextWithSpan(context.Background(), span) + req := httptest.NewRequest("GET", "http://example.com/hello", nil) + + _, err := rt.RoundTrip(req.WithContext(ctx)) + require.NoError(t, err) + + span.Finish() + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 2) + + assert.Equal(t, spans[1].TraceID, spans[0].TraceID) + + require.NotNil(t, spans[0].ParentID) + assert.Equal(t, spans[1].SpanID, *spans[0].ParentID) + + traceID, err := instana.Header2ID(traceIDHeader) + require.NoError(t, err) + assert.Equal(t, spans[0].TraceID, traceID) + + spanID, err := instana.Header2ID(spanIDHeader) + require.NoError(t, err) + assert.Equal(t, spans[0].SpanID, spanID) +} + +func TestRoundTripper_Error(t *testing.T) { + serverErr := errors.New("something went wrong") + + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + rt := instana.RoundTripper(s, testRoundTripper(func(req *http.Request) (*http.Response, error) { + return nil, serverErr + })) + + _, err := rt.RoundTrip(httptest.NewRequest("GET", "http://example.com/hello", nil)) + assert.Error(t, err) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.True(t, span.Error) + assert.Equal(t, 1, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "net/http.Client", span.Data.SDK.Name) + assert.Equal(t, "exit", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "message": "something went wrong", + "http.error": "something went wrong", + "http.method": "GET", + "http.url": "http://example.com/hello", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCClientEnum, + }, span.Data.SDK.Custom.Tags) + + var logRecords []map[string]interface{} + for _, v := range span.Data.SDK.Custom.Logs { + logRecords = append(logRecords, v) + } + + require.Len(t, logRecords, 1) + assert.Equal(t, serverErr, logRecords[0]["error"]) +} + +func TestRoundTripper_DefaultTransport(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + var numCalls int + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + assert.NotEmpty(t, req.Header.Get(instana.FieldT)) + assert.NotEmpty(t, req.Header.Get(instana.FieldS)) + + w.Write([]byte("OK")) + })) + defer ts.Close() + + rt := instana.RoundTripper(s, nil) + + resp, err := rt.RoundTrip(httptest.NewRequest("GET", ts.URL+"/hello", nil)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + assert.Equal(t, 1, numCalls) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.False(t, span.Error) + assert.Equal(t, 0, span.Ec) + + require.NotNil(t, span.Data) + require.NotNil(t, span.Data.SDK) + assert.Equal(t, "net/http.Client", span.Data.SDK.Name) + assert.Equal(t, "exit", span.Data.SDK.Type) + + require.NotNil(t, span.Data.SDK.Custom) + assert.Equal(t, ot.Tags{ + "http.method": "GET", + "http.status_code": http.StatusOK, + "http.url": ts.URL + "/hello", + "peer.hostname": strings.TrimPrefix(ts.URL, "http://"), + "span.kind": ext.SpanKindRPCClientEnum, + }, span.Data.SDK.Custom.Tags) +} + +type testRoundTripper func(*http.Request) (*http.Response, error) + +func (rt testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt(req) +}