Skip to content

Commit

Permalink
HTTP HandleFunc / HTTP client tracing simplified (#63)
Browse files Browse the repository at this point in the history
* HTTP HandleFunc / HTTP client tracing simplified

* Added a basic documentation of the new capabilities

* Added a one-shot registriation/wrapping function for even simpler usage

* fixed tracing to not show up in instana
  • Loading branch information
noctarius authored and pglombardo committed Apr 5, 2019
1 parent 56457d4 commit 8f7babc
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
157 changes: 157 additions & 0 deletions adapters.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
84 changes: 84 additions & 0 deletions example/webserver/instana/http.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
File renamed without changes.

0 comments on commit 8f7babc

Please sign in to comment.