diff --git a/README.md b/README.md index aa697f916..c324dbde7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 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") @@ -24,6 +24,28 @@ 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. @@ -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 diff --git a/adapters.go b/adapters.go index e345bfb5f..0cd5ffb98 100644 --- a/adapters.go +++ b/adapters.go @@ -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) { @@ -52,15 +54,23 @@ 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) @@ -68,58 +78,60 @@ func (s *Sensor) TracingHttpRequest(name string, parent, req *http.Request, clie 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 { @@ -127,6 +139,8 @@ func (s *Sensor) WithTracingSpan(name string, w http.ResponseWriter, req *http.R } else { span.LogFields(otlog.Object("error", err)) } + + // re-throw the panic panic(err) } }() @@ -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)) }) } diff --git a/adapters_test.go b/adapters_test.go new file mode 100644 index 000000000..6a9bd149c --- /dev/null +++ b/adapters_test.go @@ -0,0 +1,257 @@ +package instana_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "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 TestSensor_TracingHandler_Write(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{ + Service: "go-sensor-test", + }, recorder)) + + h := s.TracingHandler("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) +} + +func TestSensor_TracingHandler_WriteHeaders(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + h := s.TracingHandler("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": 501, + "http.url": "/test", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCServerEnum, + }, span.Data.SDK.Custom.Tags) +} + +func TestTracingHttpRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + http.Error(w, "Not Found", http.StatusNotFound) + })) + defer ts.Close() + + tsURL, err := url.Parse(ts.URL) + require.NoError(t, err) + + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + req, err := http.NewRequest("GET", ts.URL+"/path?q=s", nil) + require.NoError(t, err) + + resp, err := s.TracingHttpRequest("test-request", httptest.NewRequest("GET", "/parent", nil), req, http.Client{}) + require.NoError(t, err) + + assert.Equal(t, http.StatusNotFound, 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, "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": 404, + "http.url": ts.URL + "/path?q=s", + "peer.hostname": tsURL.Host, + "span.kind": ext.SpanKindRPCClientEnum, + }, span.Data.SDK.Custom.Tags) +} + +func TestWithTracingSpan(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + + s.WithTracingSpan("test-span", rec, req, func(sp ot.Span) { + sp.SetTag("custom-tag", "value") + }) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.Nil(t, span.ParentID) + 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-span", 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.url": "/test", + "peer.hostname": "example.com", + "span.kind": ext.SpanKindRPCServerEnum, + "custom-tag": "value", + }, span.Data.SDK.Custom.Tags) +} + +func TestWithTracingSpan_PanicHandling(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + + require.Panics(t, func() { + s.WithTracingSpan("test-span", rec, req, func(sp ot.Span) { + panic("something went wrong") + }) + }) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + span := spans[0] + assert.Nil(t, span.ParentID) + 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-span", 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.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) + } + assert.Contains(t, logRecords, map[string]interface{}{"error": "something went wrong"}) +} + +func TestWithTracingSpan_WithActiveParentSpan(t *testing.T) { + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + s := instana.NewSensorWithTracer(tracer) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + + parentSpan := tracer.StartSpan("parent-span") + ctx := instana.ContextWithSpan(req.Context(), parentSpan) + + s.WithTracingSpan("test-span", rec, req.WithContext(ctx), func(sp ot.Span) {}) + parentSpan.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) +} + +func TestWithTracingSpan_WithWireContext(t *testing.T) { + recorder := instana.NewTestRecorder() + s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{}, recorder)) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test", nil) + + traceID, err := instana.ID2Header(1234567890) + require.NoError(t, err) + + parentSpanID, err := instana.ID2Header(1) + require.NoError(t, err) + + req.Header.Set(instana.FieldT, traceID) + req.Header.Set(instana.FieldS, parentSpanID) + + s.WithTracingSpan("test-span", rec, req, func(sp ot.Span) {}) + + spans := recorder.GetQueuedSpans() + require.Len(t, spans, 1) + + assert.Equal(t, int64(1234567890), spans[0].TraceID) + + require.NotNil(t, spans[0].ParentID) + assert.Equal(t, int64(1), *spans[0].ParentID) +} + +func TestWithTracingContext(t *testing.T) {} diff --git a/context.go b/context.go index c5b86f7dc..98888278a 100644 --- a/context.go +++ b/context.go @@ -1,42 +1,27 @@ package instana -// SpanContext holds the basic Span metadata. -type SpanContext struct { - // A probabilistically unique identifier for a [multi-span] trace. - TraceID int64 +import ( + "context" - // A probabilistically unique identifier for a span. - SpanID int64 + ot "github.com/opentracing/opentracing-go" +) - // Whether the trace is sampled. - Sampled bool +type contextKey struct{} - // The span's associated baggage. - Baggage map[string]string // initialized on first use -} +var activeSpanKey contextKey -// ForeachBaggageItem belongs to the opentracing.SpanContext interface -func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { - for k, v := range c.Baggage { - if !handler(k, v) { - break - } - } +// ContextWithSpan returns a new context.Context holding a reference to an active span +func ContextWithSpan(ctx context.Context, sp ot.Span) context.Context { + return context.WithValue(ctx, activeSpanKey, sp) } -// WithBaggageItem returns an entirely new SpanContext with the -// given key:value baggage pair set. -func (c SpanContext) WithBaggageItem(key, val string) SpanContext { - var newBaggage map[string]string - if c.Baggage == nil { - newBaggage = map[string]string{key: val} - } else { - newBaggage = make(map[string]string, len(c.Baggage)+1) - for k, v := range c.Baggage { - newBaggage[k] = v - } - newBaggage[key] = val +// SpanFromContext retrieves previously stored active span from context. If there is no +// span, this method returns false. +func SpanFromContext(ctx context.Context) (ot.Span, bool) { + sp, ok := ctx.Value(activeSpanKey).(ot.Span) + if !ok { + return nil, false } - // Use positional parameters so the compiler will help catch new fields. - return SpanContext{c.TraceID, c.SpanID, c.Sampled, newBaggage} + + return sp, true } diff --git a/context_test.go b/context_test.go new file mode 100644 index 000000000..401c62e4b --- /dev/null +++ b/context_test.go @@ -0,0 +1,27 @@ +package instana_test + +import ( + "context" + "testing" + + instana "github.com/instana/go-sensor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpanFromContext_WithActiveSpan(t *testing.T) { + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + + span := tracer.StartSpan("test") + ctx := instana.ContextWithSpan(context.Background(), span) + + sp, ok := instana.SpanFromContext(ctx) + require.True(t, ok) + assert.Equal(t, span, sp) +} + +func TestSpanFromContext_NoActiveSpan(t *testing.T) { + _, ok := instana.SpanFromContext(context.Background()) + assert.False(t, ok) +} diff --git a/span.go b/span.go index 3c6c71c99..d8b307b24 100644 --- a/span.go +++ b/span.go @@ -12,10 +12,6 @@ import ( ) type spanS struct { - tracer *tracerS - sync.Mutex - - context SpanContext ParentSpanID int64 Operation string Start time.Time @@ -24,11 +20,16 @@ type spanS struct { Logs []ot.LogRecord Error bool Ec int + + tracer *tracerS + mu sync.Mutex + + context SpanContext } func (r *spanS) BaggageItem(key string) string { - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() return r.context.Baggage[key] } @@ -38,8 +39,8 @@ func (r *spanS) SetBaggageItem(key, val string) ot.Span { return r } - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() r.context = r.context.WithBaggageItem(key, val) return r @@ -60,8 +61,10 @@ func (r *spanS) FinishWithOptions(opts ot.FinishOptions) { } duration := finishTime.Sub(r.Start) - r.Lock() - defer r.Unlock() + + r.mu.Lock() + defer r.mu.Unlock() + for _, lr := range opts.LogRecords { r.appendLog(lr) } @@ -82,8 +85,9 @@ func (r *spanS) appendLog(lr ot.LogRecord) { } func (r *spanS) Log(ld ot.LogData) { - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() + if r.trim() || r.tracer.options.DropAllLogs { return } @@ -124,8 +128,8 @@ func (r *spanS) LogFields(fields ...otlog.Field) { Fields: fields, } - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() if r.trim() || r.tracer.options.DropAllLogs { return } @@ -149,16 +153,18 @@ func (r *spanS) LogKV(keyValues ...interface{}) { } func (r *spanS) SetOperationName(operationName string) ot.Span { - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() + r.Operation = operationName return r } func (r *spanS) SetTag(key string, value interface{}) ot.Span { - r.Lock() - defer r.Unlock() + r.mu.Lock() + defer r.mu.Unlock() + if r.trim() { return r } diff --git a/span_context.go b/span_context.go new file mode 100644 index 000000000..c5b86f7dc --- /dev/null +++ b/span_context.go @@ -0,0 +1,42 @@ +package instana + +// SpanContext holds the basic Span metadata. +type SpanContext struct { + // A probabilistically unique identifier for a [multi-span] trace. + TraceID int64 + + // A probabilistically unique identifier for a span. + SpanID int64 + + // Whether the trace is sampled. + Sampled bool + + // The span's associated baggage. + Baggage map[string]string // initialized on first use +} + +// ForeachBaggageItem belongs to the opentracing.SpanContext interface +func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { + for k, v := range c.Baggage { + if !handler(k, v) { + break + } + } +} + +// WithBaggageItem returns an entirely new SpanContext with the +// given key:value baggage pair set. +func (c SpanContext) WithBaggageItem(key, val string) SpanContext { + var newBaggage map[string]string + if c.Baggage == nil { + newBaggage = map[string]string{key: val} + } else { + newBaggage = make(map[string]string, len(c.Baggage)+1) + for k, v := range c.Baggage { + newBaggage[k] = v + } + newBaggage[key] = val + } + // Use positional parameters so the compiler will help catch new fields. + return SpanContext{c.TraceID, c.SpanID, c.Sampled, newBaggage} +}