Skip to content

Commit

Permalink
Merge pull request #86 from instana/span_propagation_in_context
Browse files Browse the repository at this point in the history
Span propagation in `context.Context`
  • Loading branch information
Andrew Slotin authored Mar 11, 2020
2 parents 5d055d0 + 1765429 commit ece91bd
Show file tree
Hide file tree
Showing 7 changed files with 466 additions and 113 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,36 @@ The Instana Go sensor consists of two parts:

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.
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 instance only. The sensor implementation is fully thread-safe and can be shared by multiple threads.

```go
var sensor = instana.NewSensor("my-service")
```

A full example can be found under the examples folder in [example/webserver/instana/http.go](./example/webserver/instana/http.go).

### Trace Context Propagation

Instana Go sensor provides an API to propagate the trace context throughout the call chain:

```go
func MyFunc(ctx context.Context) {
var spanOpts []ot.StartSpanOption

// retrieve parent span from context and reference it in the new one
if parent, ok := instana.SpanFromContext(); ok {
spanOpts = append(spanOpts, ot.ChildOf(parent.Context()))
}

// start a new span
span := tracer.StartSpan("my-func", spanOpts...)
defer span.Finish()

// and use it as a new parent inside the context
SubCall(instana.ContextWithSpan(ctx, span))
}
```

### 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.
Expand Down Expand Up @@ -60,7 +82,8 @@ func main() {
// Accessing the parent request inside a handler
func myHandler(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
parentSpan := ctx.Value("parentSpan").(ot.Span) // use this TracingHttpRequest
parent, _ := instana.SpanFromContext(ctx))

tracer := parent.Tracer()
spanCtx := parent.Context().(instana.SpanContext)
traceID := spanCtx.TraceID // use this with EumSnippet
Expand Down
135 changes: 74 additions & 61 deletions adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,34 @@ import (
type SpanSensitiveFunc func(span ot.Span)
type ContextSensitiveFunc func(span ot.Span, ctx context.Context)

// Sensor is used to inject tracing information into requests
type Sensor struct {
tracer ot.Tracer
}

// Creates a new Instana sensor instance which can be used to
// inject tracing information into requests.
// NewSensor creates a new instana.Sensor
func NewSensor(serviceName string) *Sensor {
return &Sensor{
NewTracerWithOptions(
&Options{
Service: serviceName,
},
),
}
return NewSensorWithTracer(NewTracerWithOptions(
&Options{
Service: serviceName,
},
))
}

// NewSensorWithTracer returns a new instana.Sensor that uses provided tracer to report spans
func NewSensorWithTracer(tracer ot.Tracer) *Sensor {
return &Sensor{tracer: tracer}
}

// 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.
// 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()
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.
// TracingHandler 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) {
Expand All @@ -52,81 +54,93 @@ func (s *Sensor) TracingHandler(name string, handler http.HandlerFunc) http.Hand
}
}

// 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")
// TracingHttpRequest 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) (*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()))

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)

res, err := client.Do(req.WithContext(context.Background()))
if err != nil {
span.LogFields(otlog.Error(err))
return res, 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")
span.SetTag(string(ext.HTTPStatusCode), res.StatusCode)

if name == "" {
return res, nil
}

// 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
func (s *Sensor) WithTracingSpan(operationName string, w http.ResponseWriter, req *http.Request, f SpanSensitiveFunc) {
if operationName == "" {
pc, _, _, _ := runtime.Caller(1)
f := runtime.FuncForPC(pc)
name = f.Name()
operationName = f.Name()
}

opts := []ot.StartSpanOption{
ext.SpanKindRPCServer,

ot.Tags{
string(ext.PeerHostname): req.Host,
string(ext.HTTPUrl): req.URL.Path,
string(ext.HTTPMethod): req.Method,
},
}

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),
)
wireContext, err := s.tracer.Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(req.Header))
switch err {
case nil:
opts = append(opts, ext.RPCServerOption(wireContext))
case ot.ErrSpanContextNotFound:
log.debug("no span context provided with %s %s", req.Method, req.URL.Path)
case ot.ErrUnsupportedFormat:
log.info("unsupported span context format provided with %s %s", req.Method, req.URL.Path)
default:
log.warn("failed to extract span context from the request:", err)
}

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)
if ps, ok := SpanFromContext(req.Context()); ok {
opts = append(opts, ot.ChildOf(ps.Context()))
}

span := s.tracer.StartSpan(operationName, opts...)
defer span.Finish()

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))
}

// re-throw the panic
panic(err)
}
}()
Expand All @@ -138,8 +152,7 @@ func (s *Sensor) WithTracingSpan(name string, w http.ResponseWriter, req *http.R
// 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)
f(span, ContextWithSpan(req.Context(), span))
})
}

Expand Down
Loading

0 comments on commit ece91bd

Please sign in to comment.