-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HTTP HandleFunc / HTTP client tracing simplified (#63)
* 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
1 parent
56457d4
commit 8f7babc
Showing
4 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.