From 83935b25589a1a8e0219b8f845d72724853a3ead Mon Sep 17 00:00:00 2001 From: rghetia Date: Mon, 23 Sep 2019 11:51:32 -0700 Subject: [PATCH] Add propagator interface and W3C propagator (#85) * add propagation api. * add http propagator interface and w3c propagator implementation. * remove Extract api from trace. * remove Extract interface for tracer. * fix copyright. * fix variable names and comments. * move inject/extract out of trace. * replace INVALID_SPAN_CONTEXT with EmptySpanContext function. * fix tag.Map. * make carrier as interface instead of http.Request. * rename structs and update doc comments.. * add doc.go * update doc. * add noop propagator. * add new propagation api with Supplier interface. - added Default Tracer which simply propagates SpanContext. - added CopyOfRemote option to simply create remote span. * remove old propagator. * rename propagator to TextFormatPropagator. * rename default tracer/span as pass_through tracer/span. * add test for pass through tracer. * add missing interface to pass through tracer. * return SpanContext instead of contex.Context from Extract interface. - also remove PassThroughTracer * fix review comments. * add more test cases for traceContext extraction. * remove tidy temporarily from circle-ci target to avoid build failure. * allow header ending in dash '-'. * add inject test for non-zero value other than 01 for traceoption * add AddLink and Link interface to MockSpan * fix running go mod tidy on every build. --- api/propagation/doc.go | 16 + api/propagation/noop_propagator.go | 40 +++ api/propagation/propagator.go | 52 ++++ api/trace/api.go | 22 -- api/trace/noop_trace.go | 4 - example/basic/go.sum | 1 - example/http/client/client.go | 7 +- example/http/go.sum | 2 - example/http/server/server.go | 2 +- experimental/streaming/sdk/trace.go | 5 - go.mod | 1 - go.sum | 2 - internal/trace/mock_span.go | 96 ++++++ internal/trace/mock_tracer.go | 91 ++++++ plugin/httptrace/api.go | 6 +- plugin/httptrace/httptrace.go | 66 +--- propagation/doc.go | 16 + propagation/http_trace_context_propagator.go | 138 +++++++++ .../http_trace_context_propagator_test.go | 286 ++++++++++++++++++ sdk/trace/tracer.go | 5 - 20 files changed, 749 insertions(+), 109 deletions(-) create mode 100644 api/propagation/doc.go create mode 100644 api/propagation/noop_propagator.go create mode 100644 api/propagation/propagator.go create mode 100644 internal/trace/mock_span.go create mode 100644 internal/trace/mock_tracer.go create mode 100644 propagation/doc.go create mode 100644 propagation/http_trace_context_propagator.go create mode 100644 propagation/http_trace_context_propagator_test.go diff --git a/api/propagation/doc.go b/api/propagation/doc.go new file mode 100644 index 00000000000..93217e6bbd6 --- /dev/null +++ b/api/propagation/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package propagation contains interface definition for Binary and TextFormat propagators. +package propagation // import "go.opentelemetry.io/api/propagation" diff --git a/api/propagation/noop_propagator.go b/api/propagation/noop_propagator.go new file mode 100644 index 00000000000..dc185c80c92 --- /dev/null +++ b/api/propagation/noop_propagator.go @@ -0,0 +1,40 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation + +import ( + "context" + + "go.opentelemetry.io/api/core" +) + +// NoopTextFormatPropagator implements TextFormatPropagator that does nothing. +type NoopTextFormatPropagator struct{} + +var _ TextFormatPropagator = NoopTextFormatPropagator{} + +// Inject does nothing. +func (np NoopTextFormatPropagator) Inject(ctx context.Context, supplier Supplier) { +} + +// Extract does nothing and returns an empty SpanContext +func (np NoopTextFormatPropagator) Extract(ctx context.Context, supplier Supplier) core.SpanContext { + return core.EmptySpanContext() +} + +// GetAllKeys returns empty list of strings. +func (np NoopTextFormatPropagator) GetAllKeys() []string { + return []string{} +} diff --git a/api/propagation/propagator.go b/api/propagation/propagator.go new file mode 100644 index 00000000000..f52a51ed92a --- /dev/null +++ b/api/propagation/propagator.go @@ -0,0 +1,52 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation + +import ( + "context" + + "go.opentelemetry.io/api/core" +) + +// TextFormatPropagator is an interface that specifies methods to inject and extract SpanContext +// into/from a carrier using Supplier interface. +// For example, HTTP Trace Context propagator would encode SpanContext into W3C Trace +// Context Header and set the header into HttpRequest. +type TextFormatPropagator interface { + // Inject method retrieves current SpanContext from the ctx, encodes it into propagator + // specific format and then injects the encoded SpanContext using supplier into a carrier + // associated with the supplier. + Inject(ctx context.Context, supplier Supplier) + + // Extract method retrieves encoded SpanContext using supplier from the associated carrier. + // It decodes the SpanContext and returns it. If no SpanContext was retrieved OR + // if the retrieved SpanContext is invalid then an empty SpanContext is returned. + Extract(ctx context.Context, supplier Supplier) core.SpanContext + + // GetAllKeys returns all the keys that this propagator injects/extracts into/from a + // carrier. The use cases for this are + // * allow pre-allocation of fields, especially in systems like gRPC Metadata + // * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap) + GetAllKeys() []string +} + +// Supplier is an interface that specifies methods to retrieve and store +// value for a key to an associated carrier. +// Get method retrieves the value for a given key. +// Set method stores the value for a given key. +type Supplier interface { + Get(key string) string + Set(key string, value string) +} diff --git a/api/trace/api.go b/api/trace/api.go index afb49fa42a5..68cba1ea990 100644 --- a/api/trace/api.go +++ b/api/trace/api.go @@ -44,9 +44,6 @@ type Tracer interface { // WithResources attaches resource attributes to the Tracer. WithResources(res ...core.KeyValue) Tracer - - // Note: see https://github.com/opentracing/opentracing-go/issues/127 - Inject(context.Context, Span, Injector) } type FinishOptions struct { @@ -102,13 +99,6 @@ type Span interface { ModifyAttributes(...tag.Mutator) } -type Injector interface { - // Inject serializes span context and tag.Map and inserts them in to - // carrier associated with the injector. For example in case of http request, - // span context could added to the request (carrier) as W3C Trace context header. - Inject(core.SpanContext, tag.Map) -} - // SpanOption apply changes to SpanOptions. type SpanOption func(*SpanOptions) @@ -157,18 +147,6 @@ func Start(ctx context.Context, name string, opts ...SpanOption) (context.Contex return GlobalTracer().Start(ctx, name, opts...) } -// Inject is convenient function to inject current span context using injector. -// Injector is expected to serialize span context and inject it in to a carrier. -// An example of a carrier is http request. -func Inject(ctx context.Context, injector Injector) { - span := CurrentSpan(ctx) - if span == nil { - return - } - - span.Tracer().Inject(ctx, span, injector) -} - // WithStartTime sets the start time of the span to provided time t, when it is started. // In absensce of this option, wall clock time is used as start time. // This option is typically used when starting of the span is delayed. diff --git a/api/trace/noop_trace.go b/api/trace/noop_trace.go index 8a590232300..b99c0773d45 100644 --- a/api/trace/noop_trace.go +++ b/api/trace/noop_trace.go @@ -49,7 +49,3 @@ func (NoopTracer) Start(ctx context.Context, name string, opts ...SpanOption) (c span := NoopSpan{} return SetCurrentSpan(ctx, span), span } - -// Inject does nothing. -func (NoopTracer) Inject(ctx context.Context, span Span, injector Injector) { -} diff --git a/example/basic/go.sum b/example/basic/go.sum index 8c722a62d3b..cf930d5b072 100644 --- a/example/basic/go.sum +++ b/example/basic/go.sum @@ -77,7 +77,6 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= diff --git a/example/http/client/client.go b/example/http/client/client.go index 0dc8e0ea8b9..6be1d347412 100644 --- a/example/http/client/client.go +++ b/example/http/client/client.go @@ -17,6 +17,7 @@ package main import ( "context" "fmt" + "go.opentelemetry.io/plugin/httptrace" "io/ioutil" "net/http" @@ -25,7 +26,6 @@ import ( "go.opentelemetry.io/api/key" "go.opentelemetry.io/api/tag" "go.opentelemetry.io/api/trace" - "go.opentelemetry.io/plugin/httptrace" ) var ( @@ -50,9 +50,8 @@ func main() { func(ctx context.Context) error { req, _ := http.NewRequest("GET", "http://localhost:7777/hello", nil) - ctx, req, inj := httptrace.W3C(ctx, req) - - trace.Inject(ctx, inj) + ctx, req = httptrace.W3C(ctx, req) + httptrace.Inject(ctx, req) res, err := client.Do(req) if err != nil { diff --git a/example/http/go.sum b/example/http/go.sum index 640c478f697..8af8206fe58 100644 --- a/example/http/go.sum +++ b/example/http/go.sum @@ -81,8 +81,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac h1:+2b6iGRJe3hvV/yVXrd41yVEjxuFHxasJqDhkIjS4gk= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= diff --git a/example/http/server/server.go b/example/http/server/server.go index dbab3b10113..0a3d099de3d 100644 --- a/example/http/server/server.go +++ b/example/http/server/server.go @@ -35,7 +35,7 @@ var ( func main() { helloHandler := func(w http.ResponseWriter, req *http.Request) { - attrs, tags, spanCtx := httptrace.Extract(req) + attrs, tags, spanCtx := httptrace.Extract(req.Context(), req) req = req.WithContext(tag.WithMap(req.Context(), tag.NewMap(tag.MapUpdate{ MultiKV: tags, diff --git a/experimental/streaming/sdk/trace.go b/experimental/streaming/sdk/trace.go index 40e2a8c5c41..4371389f702 100644 --- a/experimental/streaming/sdk/trace.go +++ b/experimental/streaming/sdk/trace.go @@ -20,7 +20,6 @@ import ( "go.opentelemetry.io/api/core" "go.opentelemetry.io/api/key" - "go.opentelemetry.io/api/tag" "go.opentelemetry.io/api/trace" apitrace "go.opentelemetry.io/api/trace" "go.opentelemetry.io/experimental/streaming/exporter/observer" @@ -125,7 +124,3 @@ func (t *tracer) Start(ctx context.Context, name string, opts ...apitrace.SpanOp } return trace.SetCurrentSpan(ctx, span), span } - -func (t *tracer) Inject(ctx context.Context, span apitrace.Span, injector apitrace.Injector) { - injector.Inject(span.SpanContext(), tag.FromContext(ctx)) -} diff --git a/go.mod b/go.mod index 052874d78bf..638e0be25a7 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/golangci/golangci-lint v1.17.1 github.com/google/go-cmp v0.3.0 github.com/hashicorp/golang-lru v0.5.3 - github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 google.golang.org/api v0.9.0 google.golang.org/grpc v1.22.1 diff --git a/go.sum b/go.sum index b03d2e062f0..d1d45805931 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac h1:+2b6iGRJe3hvV/yVXrd41yVEjxuFHxasJqDhkIjS4gk= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/internal/trace/mock_span.go b/internal/trace/mock_span.go new file mode 100644 index 00000000000..04858b51d99 --- /dev/null +++ b/internal/trace/mock_span.go @@ -0,0 +1,96 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + + "google.golang.org/grpc/codes" + + "go.opentelemetry.io/api/core" + "go.opentelemetry.io/api/tag" + apitrace "go.opentelemetry.io/api/trace" +) + +// MockSpan is a mock span used in association with MockTracer for testing purpose only. +type MockSpan struct { + sc core.SpanContext + tracer apitrace.Tracer +} + +var _ apitrace.Span = (*MockSpan)(nil) + +// SpanContext returns associated core.SpanContext. If the receiver is nil it returns +// an empty core.SpanContext +func (ms *MockSpan) SpanContext() core.SpanContext { + if ms == nil { + core.EmptySpanContext() + } + return ms.sc +} + +// IsRecordingEvents always returns false for MockSpan. +func (ms *MockSpan) IsRecordingEvents() bool { + return false +} + +// SetStatus does nothing. +func (ms *MockSpan) SetStatus(status codes.Code) { +} + +// SetError does nothing. +func (ms *MockSpan) SetError(v bool) { +} + +// SetAttribute does nothing. +func (ms *MockSpan) SetAttribute(attribute core.KeyValue) { +} + +// SetAttributes does nothing. +func (ms *MockSpan) SetAttributes(attributes ...core.KeyValue) { +} + +// ModifyAttribute does nothing. +func (ms *MockSpan) ModifyAttribute(mutator tag.Mutator) { +} + +// ModifyAttributes does nothing. +func (ms *MockSpan) ModifyAttributes(mutators ...tag.Mutator) { +} + +// Finish does nothing. +func (ms *MockSpan) Finish(options ...apitrace.FinishOption) { +} + +// SetName does nothing. +func (ms *MockSpan) SetName(name string) { +} + +// Tracer returns MockTracer implementation of Tracer. +func (ms *MockSpan) Tracer() apitrace.Tracer { + return ms.tracer +} + +// AddEvent does nothing. +func (ms *MockSpan) AddEvent(ctx context.Context, msg string, attrs ...core.KeyValue) { +} + +// AddLink does nothing. +func (ms *MockSpan) AddLink(link apitrace.Link) { +} + +// Link does nothing. +func (ms *MockSpan) Link(sc core.SpanContext, attrs ...core.KeyValue) { +} diff --git a/internal/trace/mock_tracer.go b/internal/trace/mock_tracer.go new file mode 100644 index 00000000000..101748bd2a0 --- /dev/null +++ b/internal/trace/mock_tracer.go @@ -0,0 +1,91 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trace + +import ( + "context" + "math/rand" + "sync/atomic" + + "go.opentelemetry.io/api/core" + apitrace "go.opentelemetry.io/api/trace" +) + +// MockTracer is a simple tracer used for testing purpose only. +// It only supports ChildOf option. SpanId is atomically increased every time a +// new span is created. +type MockTracer struct { + // Sampled specifies if the new span should be sampled or not. + Sampled bool + + // StartSpanId is used to initialize spanId. It is incremented by one + // every time a new span is created. + StartSpanId *uint64 +} + +var _ apitrace.Tracer = (*MockTracer)(nil) + +// WithResources does nothing and returns MockTracer implementation of Tracer. +func (mt *MockTracer) WithResources(attributes ...core.KeyValue) apitrace.Tracer { + return mt +} + +// WithComponent does nothing and returns MockTracer implementation of Tracer. +func (mt *MockTracer) WithComponent(name string) apitrace.Tracer { + return mt +} + +// WithService does nothing and returns MockTracer implementation of Tracer. +func (mt *MockTracer) WithService(name string) apitrace.Tracer { + return mt +} + +// WithSpan does nothing except executing the body. +func (mt *MockTracer) WithSpan(ctx context.Context, name string, body func(context.Context) error) error { + return body(ctx) +} + +// Start starts a MockSpan. It creates a new Span based on Reference SpanContext option. +// TracdID is used from Reference Span Context and SpanID is assigned. +// If Reference SpanContext option is not specified then random TraceID is used. +// No other options are supported. +func (mt *MockTracer) Start(ctx context.Context, name string, o ...apitrace.SpanOption) (context.Context, apitrace.Span) { + var opts apitrace.SpanOptions + for _, op := range o { + op(&opts) + } + var span *MockSpan + var sc core.SpanContext + if !opts.Reference.SpanContext.IsValid() { + sc = core.SpanContext{ + TraceID: core.TraceID{ + High: rand.Uint64(), + Low: rand.Uint64(), + }, + } + if mt.Sampled { + sc.TraceOptions = core.TraceOptionSampled + } + } else { + sc = opts.Reference.SpanContext + } + sc.SpanID = atomic.AddUint64(mt.StartSpanId, 1) + span = &MockSpan{ + sc: sc, + tracer: mt, + } + + return apitrace.SetCurrentSpan(ctx, span), span +} diff --git a/plugin/httptrace/api.go b/plugin/httptrace/api.go index da3a0d40131..3fb84c30ef0 100644 --- a/plugin/httptrace/api.go +++ b/plugin/httptrace/api.go @@ -18,12 +18,10 @@ import ( "context" "net/http" "net/http/httptrace" - - "go.opentelemetry.io/api/trace" ) // Client -func W3C(ctx context.Context, req *http.Request) (context.Context, *http.Request, trace.Injector) { +func W3C(ctx context.Context, req *http.Request) (context.Context, *http.Request) { t := newClientTracer(ctx) t.GetConn = t.getConn @@ -45,5 +43,5 @@ func W3C(ctx context.Context, req *http.Request) (context.Context, *http.Request ctx = httptrace.WithClientTrace(ctx, &t.ClientTrace) req = req.WithContext(ctx) - return ctx, req, hinjector{req} + return ctx, req } diff --git a/plugin/httptrace/httptrace.go b/plugin/httptrace/httptrace.go index c2287c297a3..86a64c5cd1f 100644 --- a/plugin/httptrace/httptrace.go +++ b/plugin/httptrace/httptrace.go @@ -15,15 +15,12 @@ package httptrace import ( - "encoding/binary" + "context" "net/http" - "github.com/lightstep/tracecontext.go" - "github.com/lightstep/tracecontext.go/tracestate" - "go.opentelemetry.io/api/core" "go.opentelemetry.io/api/key" - "go.opentelemetry.io/api/tag" + "go.opentelemetry.io/propagation" ) const ( @@ -34,68 +31,21 @@ var ( HostKey = key.New("http.host") URLKey = key.New("http.url") - encoding = binary.BigEndian + propagator = propagation.HttpTraceContextPropagator() ) // Returns the Attributes, Context Tags, and SpanContext that were encoded by Inject. -func Extract(req *http.Request) ([]core.KeyValue, []core.KeyValue, core.SpanContext) { - tc, err := tracecontext.FromHeaders(req.Header) - - if err != nil { - return nil, nil, core.SpanContext{} - } - - var sc core.SpanContext - sc.SpanID = encoding.Uint64(tc.TraceParent.SpanID[0:8]) - sc.TraceID.High = encoding.Uint64(tc.TraceParent.TraceID[0:8]) - sc.TraceID.Low = encoding.Uint64(tc.TraceParent.TraceID[8:16]) +func Extract(ctx context.Context, req *http.Request) ([]core.KeyValue, []core.KeyValue, core.SpanContext) { + sc := propagator.Extract(ctx, req.Header) attrs := []core.KeyValue{ URLKey.String(req.URL.String()), // Etc. } - var tags []core.KeyValue - - for _, ts := range tc.TraceState { - if ts.Vendor != Vendor { - continue - } - // TODO: max-hops, type conversion questions answered, - // case-conversion questions. - tags = append(tags, key.New(ts.Tenant).String(ts.Value)) - } - - return attrs, tags, sc + return attrs, nil, sc } -type hinjector struct { - *http.Request -} - -func (h hinjector) Inject(sc core.SpanContext, tags tag.Map) { - var tc tracecontext.TraceContext - var sid [8]byte - var tid [16]byte - - encoding.PutUint64(sid[0:8], sc.SpanID) - encoding.PutUint64(tid[0:8], sc.TraceID.High) - encoding.PutUint64(tid[8:16], sc.TraceID.Low) - - tc.TraceParent.Version = tracecontext.Version - tc.TraceParent.TraceID = tid - tc.TraceParent.SpanID = sid - tc.TraceParent.Flags.Recorded = true // Note: not implemented. - - tags.Foreach(func(kv core.KeyValue) bool { - // TODO: implement MaxHops - tc.TraceState = append(tc.TraceState, tracestate.Member{ - Vendor: Vendor, - Tenant: kv.Key.Name, - Value: kv.Value.Emit(), - }) - return true - }) - - tc.SetHeaders(h.Header) +func Inject(ctx context.Context, req *http.Request) { + propagator.Inject(ctx, req.Header) } diff --git a/propagation/doc.go b/propagation/doc.go new file mode 100644 index 00000000000..72296b3b6fc --- /dev/null +++ b/propagation/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package propagation contains propagators for different format and carriers. +package propagation // import "go.opentelemetry.io/propagation" diff --git a/propagation/http_trace_context_propagator.go b/propagation/http_trace_context_propagator.go new file mode 100644 index 00000000000..7ce6f614aeb --- /dev/null +++ b/propagation/http_trace_context_propagator.go @@ -0,0 +1,138 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation + +import ( + "context" + "encoding/hex" + "fmt" + "regexp" + "strconv" + "strings" + + "go.opentelemetry.io/api/trace" + + "go.opentelemetry.io/api/core" + apipropagation "go.opentelemetry.io/api/propagation" +) + +const ( + supportedVersion = 0 + maxVersion = 254 + traceparentHeader = "traceparent" +) + +type httpTraceContextPropagator struct{} + +var _ apipropagation.TextFormatPropagator = httpTraceContextPropagator{} +var traceCtxRegExp = regexp.MustCompile("^[0-9a-f]{2}-[a-f0-9]{32}-[a-f0-9]{16}-[a-f0-9]{2}-?") + +func (hp httpTraceContextPropagator) Inject(ctx context.Context, supplier apipropagation.Supplier) { + sc := trace.CurrentSpan(ctx).SpanContext() + if sc.IsValid() { + h := fmt.Sprintf("%.2x-%.16x%.16x-%.16x-%.2x", + supportedVersion, + sc.TraceID.High, + sc.TraceID.Low, + sc.SpanID, + sc.TraceOptions&core.TraceOptionSampled) + supplier.Set(traceparentHeader, h) + } +} + +func (hp httpTraceContextPropagator) Extract(ctx context.Context, supplier apipropagation.Supplier) core.SpanContext { + h := supplier.Get(traceparentHeader) + if h == "" { + return core.EmptySpanContext() + } + + h = strings.Trim(h, "-") + if !traceCtxRegExp.MatchString(h) { + return core.EmptySpanContext() + } + + sections := strings.Split(h, "-") + if len(sections) < 4 { + return core.EmptySpanContext() + } + + if len(sections[0]) != 2 { + return core.EmptySpanContext() + } + ver, err := hex.DecodeString(sections[0]) + if err != nil { + return core.EmptySpanContext() + } + version := int(ver[0]) + if version > maxVersion { + return core.EmptySpanContext() + } + + if version == 0 && len(sections) != 4 { + return core.EmptySpanContext() + } + + if len(sections[1]) != 32 { + return core.EmptySpanContext() + } + + result, err := strconv.ParseUint(sections[1][0:16], 16, 64) + if err != nil { + return core.EmptySpanContext() + } + var sc core.SpanContext + + sc.TraceID.High = result + + result, err = strconv.ParseUint(sections[1][16:32], 16, 64) + if err != nil { + return core.EmptySpanContext() + } + sc.TraceID.Low = result + + if len(sections[2]) != 16 { + return core.EmptySpanContext() + } + result, err = strconv.ParseUint(sections[2][0:], 16, 64) + if err != nil { + return core.EmptySpanContext() + } + sc.SpanID = result + + if len(sections[3]) != 2 { + return core.EmptySpanContext() + } + opts, err := hex.DecodeString(sections[3]) + if err != nil || len(opts) < 1 || (version == 0 && opts[0] > 2) { + return core.EmptySpanContext() + } + sc.TraceOptions = opts[0] &^ core.TraceOptionUnused + + if !sc.IsValid() { + return core.EmptySpanContext() + } + + return sc +} + +func (hp httpTraceContextPropagator) GetAllKeys() []string { + return []string{traceparentHeader} +} + +// HttpTraceContextPropagator creates a new text format propagator that propagates SpanContext +// in W3C TraceContext format. +func HttpTraceContextPropagator() apipropagation.TextFormatPropagator { + return httpTraceContextPropagator{} +} diff --git a/propagation/http_trace_context_propagator_test.go b/propagation/http_trace_context_propagator_test.go new file mode 100644 index 00000000000..65be3b95b33 --- /dev/null +++ b/propagation/http_trace_context_propagator_test.go @@ -0,0 +1,286 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package propagation_test + +import ( + "context" + "net/http" + "testing" + + "go.opentelemetry.io/api/trace" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/api/core" + mocktrace "go.opentelemetry.io/internal/trace" + "go.opentelemetry.io/propagation" +) + +var ( + traceID = core.TraceID{High: 0x4bf92f3577b34da6, Low: 0xa3ce929d0e0e4736} + spanID = uint64(0x00f067aa0ba902b7) +) + +func TestExtractValidTraceContextFromHTTPReq(t *testing.T) { + trace.SetGlobalTracer(&mocktrace.MockTracer{}) + propagator := propagation.HttpTraceContextPropagator() + tests := []struct { + name string + header string + wantSc core.SpanContext + }{ + { + name: "valid header", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + { + name: "valid header and sampled", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + { + name: "future version", + header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + { + name: "future options with sampled bit set", + header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + { + name: "future options with sampled bit cleared", + header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-08", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + }, + { + name: "future additional data", + header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-XYZxsf09", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + { + name: "valid header ending in dash", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + { + name: "future valid header ending in dash", + header: "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-", + wantSc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("traceparent", tt.header) + + ctx := context.Background() + gotSc := propagator.Extract(ctx, req.Header) + if diff := cmp.Diff(gotSc, tt.wantSc); diff != "" { + t.Errorf("Extract Tracecontext: %s: -got +want %s", tt.name, diff) + } + }) + } +} + +func TestExtractInvalidTraceContextFromHTTPReq(t *testing.T) { + trace.SetGlobalTracer(&mocktrace.MockTracer{}) + propagator := propagation.HttpTraceContextPropagator() + wantSc := core.EmptySpanContext() + tests := []struct { + name string + header string + }{ + { + name: "wrong version length", + header: "0000-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "wrong trace ID length", + header: "00-ab00000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "wrong span ID length", + header: "00-ab000000000000000000000000000000-cd0000000000000000-01", + }, + { + name: "wrong trace flag length", + header: "00-ab000000000000000000000000000000-cd00000000000000-0100", + }, + { + name: "bogus version", + header: "qw-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "bogus trace ID", + header: "00-qw000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "bogus span ID", + header: "00-ab000000000000000000000000000000-qw00000000000000-01", + }, + { + name: "bogus trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-qw", + }, + { + name: "upper case version", + header: "A0-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "upper case trace ID", + header: "00-AB000000000000000000000000000000-cd00000000000000-01", + }, + { + name: "upper case span ID", + header: "00-ab000000000000000000000000000000-CD00000000000000-01", + }, + { + name: "upper case trace flag", + header: "00-ab000000000000000000000000000000-cd00000000000000-A1", + }, + { + name: "zero trace ID and span ID", + header: "00-00000000000000000000000000000000-0000000000000000-01", + }, + { + name: "trace-flag unused bits set", + header: "00-ab000000000000000000000000000000-cd00000000000000-09", + }, + { + name: "missing options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", + }, + { + name: "empty options", + header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("traceparent", tt.header) + + ctx := context.Background() + gotSc := propagator.Extract(ctx, req.Header) + if diff := cmp.Diff(gotSc, wantSc); diff != "" { + t.Errorf("Extract Tracecontext: %s: -got +want %s", tt.name, diff) + } + }) + } +} + +func TestInjectTraceContextToHTTPReq(t *testing.T) { + var id uint64 + mockTracer := &mocktrace.MockTracer{ + Sampled: false, + StartSpanId: &id, + } + propagator := propagation.HttpTraceContextPropagator() + tests := []struct { + name string + sc core.SpanContext + wantHeader string + }{ + { + name: "valid spancontext, sampled", + sc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: core.TraceOptionSampled, + }, + wantHeader: "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000001-01", + }, + { + name: "valid spancontext, not sampled", + sc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + }, + wantHeader: "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000002-00", + }, + { + name: "valid spancontext, with unsupported bit set in traceoption", + sc: core.SpanContext{ + TraceID: traceID, + SpanID: spanID, + TraceOptions: 0xff, + }, + wantHeader: "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000003-01", + }, + { + name: "invalid spancontext", + sc: core.EmptySpanContext(), + wantHeader: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + ctx := context.Background() + if tt.sc.IsValid() { + ctx, _ = mockTracer.Start(ctx, "inject", trace.ChildOf(tt.sc)) + } + propagator.Inject(ctx, req.Header) + + gotHeader := req.Header.Get("traceparent") + if diff := cmp.Diff(gotHeader, tt.wantHeader); diff != "" { + t.Errorf("Extract Tracecontext: %s: -got +want %s", tt.name, diff) + } + }) + } +} + +func TestHttpTraceContextPropagator_GetAllKeys(t *testing.T) { + propagator := propagation.HttpTraceContextPropagator() + want := []string{"traceparent"} + got := propagator.GetAllKeys() + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("GetAllKeys: -got +want %s", diff) + } +} diff --git a/sdk/trace/tracer.go b/sdk/trace/tracer.go index a6307bd46da..74fb643e5a4 100644 --- a/sdk/trace/tracer.go +++ b/sdk/trace/tracer.go @@ -18,7 +18,6 @@ import ( "context" "go.opentelemetry.io/api/core" - "go.opentelemetry.io/api/tag" apitrace "go.opentelemetry.io/api/trace" ) @@ -102,7 +101,3 @@ func (tr *tracer) WithComponent(component string) apitrace.Tracer { tr.component = component return tr } - -func (tr *tracer) Inject(ctx context.Context, span apitrace.Span, injector apitrace.Injector) { - injector.Inject(span.SpanContext(), tag.NewEmptyMap()) -}