diff --git a/README.md b/README.md index f5dd0ed6f..d8d7b6263 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,69 @@ The Instana Go sensor consists of two parts: [![Build Status](https://travis-ci.org/instana/golang-sensor.svg?branch=master)](https://travis-ci.org/instana/golang-sensor) [![OpenTracing Badge](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) +## Common Operations + +The Instana Go sensor offers a set of quick features to support tracing of the most common operations like handling HTTP requests and executing HTTP requests. + +To create an instance of the Instana sensor just request a new instance using the _instana.NewSensor_ factory method and providing the name of the application. It is recommended to use a single Instana only. The sensor implementation is fully thread-safe and can be shared by multiple threads. + +``` +var sensor = instana.NewSensor("my-service") +``` + +A full example can be found under the examples folder in _example/webserver/instana/http.go_. + +### HTTP Server Handlers + +With support to wrap a _http.HandlerFunc_, Instana quickly adds the possibility to trace requests and collect child spans, executed in the context of the request span. + +Minimal changes are required for Instana to be able to capture the necessary information. By simply wrapping the currently existing _http.HandlerFunc_ Instana collects and injects necessary information automatically. + +That said, a simple handler function like the following will simple be wrapped and registered like normal. + +For your own preference registering the handler and wrapping it can be two separate steps or a single one. The following example code shows both versions, starting with two steps. +``` +func myHandler(w http.ResponseWriter, req *http.Request) { + time.Sleep(450 * time.Millisecond) +} + +// Doing registration and wrapping in two separate steps +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), + ) +} +``` + +### Executing HTTP Requests + +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. + +``` +req, err := http.NewRequest("GET", url, nil) +client := &http.Client{} +resp, err := sensor.TracingHttpRequest( + "myExternalCall", + parentRequest, + req, + client +) +``` + +The provided _parentRequest_ 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 _client.Do_ operation, the call will return a _http.Response_ instance or an error proving information of the failure reason. + ## Sensor To use sensor only without tracing ability, import the `instana` package and run diff --git a/adapters.go b/adapters.go new file mode 100644 index 000000000..dc8e5734d --- /dev/null +++ b/adapters.go @@ -0,0 +1,157 @@ +package instana + +import ( + "context" + "github.com/felixge/httpsnoop" + ot "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + otlog "github.com/opentracing/opentracing-go/log" + "net/http" + "runtime" +) + +type SpanSensitiveFunc func(span ot.Span) +type ContextSensitiveFunc func(span ot.Span, ctx context.Context) + +type Sensor struct { + tracer ot.Tracer +} + +// Creates a new Instana sensor instance which can be used to +// inject tracing information into requests. +func NewSensor(serviceName string) *Sensor { + return &Sensor{ + NewTracerWithOptions( + &Options{ + Service: serviceName, + }, + ), + } +} + +// It is similar to TracingHandler in regards, that it wraps an existing http.HandlerFunc +// into a named instance to support capturing tracing information and data. It, however, +// provides a neater way to register the handler with existing frameworks by returning +// not only the wrapper, but also the URL-pattern to react on. +func (s *Sensor) TraceHandler(name, pattern string, handler http.HandlerFunc) (string, http.HandlerFunc) { + return pattern, s.TracingHandler(name, handler) +} + +// Wraps an existing http.HandlerFunc into a named instance to support capturing tracing +// information and response data. +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) { + // Capture response code for span + hooks := httpsnoop.Hooks{ + WriteHeader: func(next httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { + return func(code int) { + next(code) + span.SetTag(string(ext.HTTPStatusCode), code) + } + }, + } + + // Add hooks to response writer + wrappedWriter := httpsnoop.Wrap(w, hooks) + + // Serve original handler + handler.ServeHTTP(wrappedWriter, req.WithContext(ctx)) + }) + } +} + +// Wraps an existing http.Request instance into a named instance to inject tracing and span +// header information into the actual HTTP wire transfer. +func (s *Sensor) TracingHttpRequest(name string, parent, req *http.Request, client http.Client) (res *http.Response, err error) { + var span ot.Span + if parentSpan, ok := parent.Context().Value("parentSpan").(ot.Span); ok { + span = s.tracer.StartSpan("client", ot.ChildOf(parentSpan.Context())) + } else { + span = s.tracer.StartSpan("client") + } + 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())) + + span.SetTag(string(ext.SpanKind), string(ext.SpanKindRPCClientEnum)) + span.SetTag(string(ext.PeerHostname), req.Host) + span.SetTag(string(ext.HTTPUrl), req.URL.String()) + span.SetTag(string(ext.HTTPMethod), req.Method) + span.SetTag(string(ext.HTTPStatusCode), res.StatusCode) + + if err != nil { + if e, ok := err.(error); ok { + span.LogFields(otlog.Error(e)) + } else { + span.LogFields(otlog.Object("error", err)) + } + } + return +} + +// Executes the given SpanSensitiveFunc and executes it under the scope of a child span, which is# +// injected as an argument when calling the function. +func (s *Sensor) WithTracingSpan(name string, w http.ResponseWriter, req *http.Request, f SpanSensitiveFunc) { + wireContext, _ := s.tracer.Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header)) + parentSpan := req.Context().Value("parentSpan") + + if name == "" { + pc, _, _, _ := runtime.Caller(1) + f := runtime.FuncForPC(pc) + name = f.Name() + } + + var span ot.Span + if ps, ok := parentSpan.(ot.Span); ok { + span = s.tracer.StartSpan( + name, + ext.RPCServerOption(wireContext), + ot.ChildOf(ps.Context()), + ) + } else { + span = s.tracer.StartSpan( + name, + ext.RPCServerOption(wireContext), + ) + } + + span.SetTag(string(ext.SpanKind), string(ext.SpanKindRPCServerEnum)) + span.SetTag(string(ext.PeerHostname), req.Host) + span.SetTag(string(ext.HTTPUrl), req.URL.Path) + span.SetTag(string(ext.HTTPMethod), req.Method) + + defer func() { + // Capture outgoing headers + s.tracer.Inject(span.Context(), ot.HTTPHeaders, ot.HTTPHeadersCarrier(w.Header())) + + // Make sure the span is sent in case we have to re-panic + defer span.Finish() + + // Be sure to capture any kind of panic / error + if err := recover(); err != nil { + if e, ok := err.(error); ok { + span.LogFields(otlog.Error(e)) + } else { + span.LogFields(otlog.Object("error", err)) + } + panic(err) + } + }() + + f(span) +} + +// 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'. +func (s *Sensor) WithTracingContext(name string, w http.ResponseWriter, req *http.Request, f ContextSensitiveFunc) { + s.WithTracingSpan(name, w, req, func(span ot.Span) { + ctx := context.WithValue(req.Context(), "parentSpan", span) + f(span, ctx) + }) +} diff --git a/example/webserver/instana/http.go b/example/webserver/instana/http.go new file mode 100644 index 000000000..763188913 --- /dev/null +++ b/example/webserver/instana/http.go @@ -0,0 +1,84 @@ +package main + +import ( + "github.com/instana/golang-sensor" + "net/http" + "time" +) + +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/http.go b/example/webserver/opentracing/http.go similarity index 100% rename from example/webserver/http.go rename to example/webserver/opentracing/http.go