diff --git a/api/correlation/correlation_context_propagator.go b/api/correlation/correlation_context_propagator.go new file mode 100644 index 00000000000..50af2bf9810 --- /dev/null +++ b/api/correlation/correlation_context_propagator.go @@ -0,0 +1,99 @@ +package correlation + +import ( + "context" + "net/url" + "strings" + + "go.opentelemetry.io/otel/api/core" + "go.opentelemetry.io/otel/api/key" + "go.opentelemetry.io/otel/api/propagation" +) + +// CorrelationContextHeader is specified by W3C. +//nolint:golint +var CorrelationContextHeader = "Correlation-Context" + +// CorrelationContext propagates Key:Values in W3C CorrelationContext +// format. +// nolint:golint +type CorrelationContext struct{} + +var _ propagation.HTTPPropagator = CorrelationContext{} + +// DefaultHTTPPropagator returns the default context correlation HTTP +// propagator. +func DefaultHTTPPropagator() propagation.HTTPPropagator { + return CorrelationContext{} +} + +// Inject implements HTTPInjector. +func (CorrelationContext) Inject(ctx context.Context, supplier propagation.HTTPSupplier) { + correlationCtx := FromContext(ctx) + firstIter := true + var headerValueBuilder strings.Builder + correlationCtx.Foreach(func(kv core.KeyValue) bool { + if !firstIter { + headerValueBuilder.WriteRune(',') + } + firstIter = false + headerValueBuilder.WriteString(url.QueryEscape(strings.TrimSpace((string)(kv.Key)))) + headerValueBuilder.WriteRune('=') + headerValueBuilder.WriteString(url.QueryEscape(strings.TrimSpace(kv.Value.Emit()))) + return true + }) + if headerValueBuilder.Len() > 0 { + headerString := headerValueBuilder.String() + supplier.Set(CorrelationContextHeader, headerString) + } +} + +// Extract implements HTTPExtractor. +func (CorrelationContext) Extract(ctx context.Context, supplier propagation.HTTPSupplier) context.Context { + correlationContext := supplier.Get(CorrelationContextHeader) + if correlationContext == "" { + return WithMap(ctx, NewEmptyMap()) + } + + contextValues := strings.Split(correlationContext, ",") + keyValues := make([]core.KeyValue, 0, len(contextValues)) + for _, contextValue := range contextValues { + valueAndProps := strings.Split(contextValue, ";") + if len(valueAndProps) < 1 { + continue + } + nameValue := strings.Split(valueAndProps[0], "=") + if len(nameValue) < 2 { + continue + } + name, err := url.QueryUnescape(nameValue[0]) + if err != nil { + continue + } + trimmedName := strings.TrimSpace(name) + value, err := url.QueryUnescape(nameValue[1]) + if err != nil { + continue + } + trimmedValue := strings.TrimSpace(value) + + // TODO (skaris): properties defiend https://w3c.github.io/correlation-context/, are currently + // just put as part of the value. + var trimmedValueWithProps strings.Builder + trimmedValueWithProps.WriteString(trimmedValue) + for _, prop := range valueAndProps[1:] { + trimmedValueWithProps.WriteRune(';') + trimmedValueWithProps.WriteString(prop) + } + + keyValues = append(keyValues, key.New(trimmedName).String(trimmedValueWithProps.String())) + } + return WithMap(ctx, NewMap(MapUpdate{ + MultiKV: keyValues, + })) +} + +// GetAllKeys implements HTTPPropagator. +func (CorrelationContext) GetAllKeys() []string { + return []string{CorrelationContextHeader} +} diff --git a/api/correlation/correlation_context_propagator_test.go b/api/correlation/correlation_context_propagator_test.go new file mode 100644 index 00000000000..c0f77379fe1 --- /dev/null +++ b/api/correlation/correlation_context_propagator_test.go @@ -0,0 +1,215 @@ +package correlation_test + +import ( + "context" + "net/http" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/api/core" + "go.opentelemetry.io/otel/api/correlation" + "go.opentelemetry.io/otel/api/key" + "go.opentelemetry.io/otel/api/propagation" +) + +func TestExtractValidDistributedContextFromHTTPReq(t *testing.T) { + props := propagation.New(propagation.WithExtractors(correlation.CorrelationContext{})) + tests := []struct { + name string + header string + wantKVs []core.KeyValue + }{ + { + name: "valid w3cHeader", + header: "key1=val1,key2=val2", + wantKVs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2"), + }, + }, + { + name: "valid w3cHeader with spaces", + header: "key1 = val1, key2 =val2 ", + wantKVs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2"), + }, + }, + { + name: "valid w3cHeader with properties", + header: "key1=val1,key2=val2;prop=1", + wantKVs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2;prop=1"), + }, + }, + { + name: "valid header with url-escaped comma", + header: "key1=val1,key2=val2%2Cval3", + wantKVs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2,val3"), + }, + }, + { + name: "valid header with an invalid header", + header: "key1=val1,key2=val2,a,val3", + wantKVs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2"), + }, + }, + { + name: "valid header with no value", + header: "key1=,key2=val2", + wantKVs: []core.KeyValue{ + key.New("key1").String(""), + key.New("key2").String("val2"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Correlation-Context", tt.header) + + ctx := context.Background() + ctx = propagation.ExtractHTTP(ctx, props, req.Header) + gotCorCtx := correlation.FromContext(ctx) + wantCorCtx := correlation.NewMap(correlation.MapUpdate{MultiKV: tt.wantKVs}) + if gotCorCtx.Len() != wantCorCtx.Len() { + t.Errorf( + "Got and Want CorCtx are not the same size %d != %d", + gotCorCtx.Len(), + wantCorCtx.Len(), + ) + } + totalDiff := "" + wantCorCtx.Foreach(func(kv core.KeyValue) bool { + val, _ := gotCorCtx.Value(kv.Key) + diff := cmp.Diff(kv, core.KeyValue{Key: kv.Key, Value: val}, cmp.AllowUnexported(core.Value{})) + if diff != "" { + totalDiff += diff + "\n" + } + return true + }) + if totalDiff != "" { + t.Errorf("Extract Tracecontext: %s: -got +want %s", tt.name, totalDiff) + } + }) + } +} + +func TestExtractInvalidDistributedContextFromHTTPReq(t *testing.T) { + props := propagation.New(propagation.WithExtractors(correlation.CorrelationContext{})) + tests := []struct { + name string + header string + }{ + { + name: "no key values", + header: "header1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Correlation-Context", tt.header) + + ctx := context.Background() + ctx = propagation.ExtractHTTP(ctx, props, req.Header) + gotCorCtx := correlation.FromContext(ctx) + if gotCorCtx.Len() != 0 { + t.Errorf("Got and Want CorCtx are not the same size %d != %d", gotCorCtx.Len(), 0) + } + }) + } +} + +func TestInjectCorrelationContextToHTTPReq(t *testing.T) { + propagator := correlation.CorrelationContext{} + props := propagation.New(propagation.WithInjectors(propagator)) + tests := []struct { + name string + kvs []core.KeyValue + wantInHeader []string + wantedLen int + }{ + { + name: "two simple values", + kvs: []core.KeyValue{ + key.New("key1").String("val1"), + key.New("key2").String("val2"), + }, + wantInHeader: []string{"key1=val1", "key2=val2"}, + }, + { + name: "two values with escaped chars", + kvs: []core.KeyValue{ + key.New("key1").String("val1,val2"), + key.New("key2").String("val3=4"), + }, + wantInHeader: []string{"key1=val1%2Cval2", "key2=val3%3D4"}, + }, + { + name: "values of non-string types", + kvs: []core.KeyValue{ + key.New("key1").Bool(true), + key.New("key2").Int(123), + key.New("key3").Int64(123), + key.New("key4").Int32(123), + key.New("key5").Uint(123), + key.New("key6").Uint32(123), + key.New("key7").Uint64(123), + key.New("key8").Float64(123.567), + key.New("key9").Float32(123.567), + }, + wantInHeader: []string{ + "key1=true", + "key2=123", + "key3=123", + "key4=123", + "key5=123", + "key6=123", + "key7=123", + "key8=123.567", + "key9=123.567", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + ctx := correlation.WithMap(context.Background(), correlation.NewMap(correlation.MapUpdate{MultiKV: tt.kvs})) + propagation.InjectHTTP(ctx, props, req.Header) + + gotHeader := req.Header.Get("Correlation-Context") + wantedLen := len(strings.Join(tt.wantInHeader, ",")) + if wantedLen != len(gotHeader) { + t.Errorf( + "%s: Inject Correlation-Context incorrect length %d != %d.", tt.name, tt.wantedLen, len(gotHeader), + ) + } + for _, inHeader := range tt.wantInHeader { + if !strings.Contains(gotHeader, inHeader) { + t.Errorf( + "%s: Inject Correlation-Context missing part of header: %s in %s", tt.name, inHeader, gotHeader, + ) + } + } + }) + } +} + +func TestTraceContextPropagator_GetAllKeys(t *testing.T) { + var propagator correlation.CorrelationContext + want := []string{"Correlation-Context"} + got := propagator.GetAllKeys() + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("GetAllKeys: -got +want %s", diff) + } +} diff --git a/api/trace/testtrace/trace_context_propagator_test.go b/api/trace/testtrace/trace_context_propagator_test.go index a3d3e86c574..6ea6f33bacf 100644 --- a/api/trace/testtrace/trace_context_propagator_test.go +++ b/api/trace/testtrace/trace_context_propagator_test.go @@ -17,14 +17,11 @@ package testtrace_test import ( "context" "net/http" - "strings" "testing" "github.com/google/go-cmp/cmp" "go.opentelemetry.io/otel/api/core" - "go.opentelemetry.io/otel/api/correlation" - "go.opentelemetry.io/otel/api/key" "go.opentelemetry.io/otel/api/propagation" "go.opentelemetry.io/otel/api/trace" mocktrace "go.opentelemetry.io/otel/internal/trace" @@ -289,202 +286,9 @@ func TestInjectTraceContextToHTTPReq(t *testing.T) { } } -func TestExtractValidDistributedContextFromHTTPReq(t *testing.T) { - propagator := trace.TraceContext{} - props := propagation.New(propagation.WithExtractors(propagator)) - tests := []struct { - name string - header string - wantKVs []core.KeyValue - }{ - { - name: "valid w3cHeader", - header: "key1=val1,key2=val2", - wantKVs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2"), - }, - }, - { - name: "valid w3cHeader with spaces", - header: "key1 = val1, key2 =val2 ", - wantKVs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2"), - }, - }, - { - name: "valid w3cHeader with properties", - header: "key1=val1,key2=val2;prop=1", - wantKVs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2;prop=1"), - }, - }, - { - name: "valid header with url-escaped comma", - header: "key1=val1,key2=val2%2Cval3", - wantKVs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2,val3"), - }, - }, - { - name: "valid header with an invalid header", - header: "key1=val1,key2=val2,a,val3", - wantKVs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2"), - }, - }, - { - name: "valid header with no value", - header: "key1=,key2=val2", - wantKVs: []core.KeyValue{ - key.New("key1").String(""), - key.New("key2").String("val2"), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest("GET", "http://example.com", nil) - req.Header.Set("Correlation-Context", tt.header) - - ctx := context.Background() - ctx = propagation.ExtractHTTP(ctx, props, req.Header) - gotCorCtx := correlation.FromContext(ctx) - wantCorCtx := correlation.NewMap(correlation.MapUpdate{MultiKV: tt.wantKVs}) - if gotCorCtx.Len() != wantCorCtx.Len() { - t.Errorf( - "Got and Want CorCtx are not the same size %d != %d", - gotCorCtx.Len(), - wantCorCtx.Len(), - ) - } - totalDiff := "" - wantCorCtx.Foreach(func(kv core.KeyValue) bool { - val, _ := gotCorCtx.Value(kv.Key) - diff := cmp.Diff(kv, core.KeyValue{Key: kv.Key, Value: val}, cmp.AllowUnexported(core.Value{})) - if diff != "" { - totalDiff += diff + "\n" - } - return true - }) - if totalDiff != "" { - t.Errorf("Extract Tracecontext: %s: -got +want %s", tt.name, totalDiff) - } - }) - } -} - -func TestExtractInvalidDistributedContextFromHTTPReq(t *testing.T) { - propagator := trace.TraceContext{} - props := propagation.New(propagation.WithExtractors(propagator)) - tests := []struct { - name string - header string - }{ - { - name: "no key values", - header: "header1", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest("GET", "http://example.com", nil) - req.Header.Set("Correlation-Context", tt.header) - - ctx := context.Background() - ctx = propagation.ExtractHTTP(ctx, props, req.Header) - gotCorCtx := correlation.FromContext(ctx) - if gotCorCtx.Len() != 0 { - t.Errorf("Got and Want CorCtx are not the same size %d != %d", gotCorCtx.Len(), 0) - } - }) - } -} - -func TestInjectCorrelationContextToHTTPReq(t *testing.T) { - propagator := trace.TraceContext{} - props := propagation.New(propagation.WithInjectors(propagator)) - tests := []struct { - name string - kvs []core.KeyValue - wantInHeader []string - wantedLen int - }{ - { - name: "two simple values", - kvs: []core.KeyValue{ - key.New("key1").String("val1"), - key.New("key2").String("val2"), - }, - wantInHeader: []string{"key1=val1", "key2=val2"}, - }, - { - name: "two values with escaped chars", - kvs: []core.KeyValue{ - key.New("key1").String("val1,val2"), - key.New("key2").String("val3=4"), - }, - wantInHeader: []string{"key1=val1%2Cval2", "key2=val3%3D4"}, - }, - { - name: "values of non-string types", - kvs: []core.KeyValue{ - key.New("key1").Bool(true), - key.New("key2").Int(123), - key.New("key3").Int64(123), - key.New("key4").Int32(123), - key.New("key5").Uint(123), - key.New("key6").Uint32(123), - key.New("key7").Uint64(123), - key.New("key8").Float64(123.567), - key.New("key9").Float32(123.567), - }, - wantInHeader: []string{ - "key1=true", - "key2=123", - "key3=123", - "key4=123", - "key5=123", - "key6=123", - "key7=123", - "key8=123.567", - "key9=123.567", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest("GET", "http://example.com", nil) - ctx := correlation.WithMap(context.Background(), correlation.NewMap(correlation.MapUpdate{MultiKV: tt.kvs})) - propagation.InjectHTTP(ctx, props, req.Header) - - gotHeader := req.Header.Get("Correlation-Context") - wantedLen := len(strings.Join(tt.wantInHeader, ",")) - if wantedLen != len(gotHeader) { - t.Errorf( - "%s: Inject Correlation-Context incorrect length %d != %d.", tt.name, tt.wantedLen, len(gotHeader), - ) - } - for _, inHeader := range tt.wantInHeader { - if !strings.Contains(gotHeader, inHeader) { - t.Errorf( - "%s: Inject Correlation-Context missing part of header: %s in %s", tt.name, inHeader, gotHeader, - ) - } - } - }) - } -} - func TestTraceContextPropagator_GetAllKeys(t *testing.T) { var propagator trace.TraceContext - want := []string{"Traceparent", "Correlation-Context"} + want := []string{"Traceparent"} got := propagator.GetAllKeys() if diff := cmp.Diff(got, want); diff != "" { t.Errorf("GetAllKeys: -got +want %s", diff) diff --git a/api/trace/trace_context_propagator.go b/api/trace/trace_context_propagator.go index d586322d8a0..99ca303baac 100644 --- a/api/trace/trace_context_propagator.go +++ b/api/trace/trace_context_propagator.go @@ -18,21 +18,17 @@ import ( "context" "encoding/hex" "fmt" - "net/url" "regexp" "strings" "go.opentelemetry.io/otel/api/core" - "go.opentelemetry.io/otel/api/correlation" - "go.opentelemetry.io/otel/api/key" "go.opentelemetry.io/otel/api/propagation" ) const ( - supportedVersion = 0 - maxVersion = 254 - TraceparentHeader = "Traceparent" - CorrelationContextHeader = "Correlation-Context" + supportedVersion = 0 + maxVersion = 254 + TraceparentHeader = "Traceparent" ) // TraceContext propagates SpanContext in W3C TraceContext format. @@ -57,31 +53,13 @@ func (TraceContext) Inject(ctx context.Context, supplier propagation.HTTPSupplie sc.TraceFlags&core.TraceFlagsSampled) supplier.Set(TraceparentHeader, h) } - - correlationCtx := correlation.FromContext(ctx) - firstIter := true - var headerValueBuilder strings.Builder - correlationCtx.Foreach(func(kv core.KeyValue) bool { - if !firstIter { - headerValueBuilder.WriteRune(',') - } - firstIter = false - headerValueBuilder.WriteString(url.QueryEscape(strings.TrimSpace((string)(kv.Key)))) - headerValueBuilder.WriteRune('=') - headerValueBuilder.WriteString(url.QueryEscape(strings.TrimSpace(kv.Value.Emit()))) - return true - }) - if headerValueBuilder.Len() > 0 { - headerString := headerValueBuilder.String() - supplier.Set(CorrelationContextHeader, headerString) - } } func (tc TraceContext) Extract(ctx context.Context, supplier propagation.HTTPSupplier) context.Context { - return correlation.WithMap(ContextWithRemoteSpanContext(ctx, tc.extractSpanContext(supplier)), tc.extractCorrelationCtx(supplier)) + return ContextWithRemoteSpanContext(ctx, tc.extract(supplier)) } -func (TraceContext) extractSpanContext(supplier propagation.HTTPSupplier) core.SpanContext { +func (TraceContext) extract(supplier propagation.HTTPSupplier) core.SpanContext { h := supplier.Get(TraceparentHeader) if h == "" { return core.EmptySpanContext() @@ -148,50 +126,6 @@ func (TraceContext) extractSpanContext(supplier propagation.HTTPSupplier) core.S return sc } -func (TraceContext) extractCorrelationCtx(supplier propagation.HTTPSupplier) correlation.Map { - correlationContext := supplier.Get(CorrelationContextHeader) - if correlationContext == "" { - return correlation.NewEmptyMap() - } - - contextValues := strings.Split(correlationContext, ",") - keyValues := make([]core.KeyValue, 0, len(contextValues)) - for _, contextValue := range contextValues { - valueAndProps := strings.Split(contextValue, ";") - if len(valueAndProps) < 1 { - continue - } - nameValue := strings.Split(valueAndProps[0], "=") - if len(nameValue) < 2 { - continue - } - name, err := url.QueryUnescape(nameValue[0]) - if err != nil { - continue - } - trimmedName := strings.TrimSpace(name) - value, err := url.QueryUnescape(nameValue[1]) - if err != nil { - continue - } - trimmedValue := strings.TrimSpace(value) - - // TODO (skaris): properties defiend https://w3c.github.io/correlation-context/, are currently - // just put as part of the value. - var trimmedValueWithProps strings.Builder - trimmedValueWithProps.WriteString(trimmedValue) - for _, prop := range valueAndProps[1:] { - trimmedValueWithProps.WriteRune(';') - trimmedValueWithProps.WriteString(prop) - } - - keyValues = append(keyValues, key.New(trimmedName).String(trimmedValueWithProps.String())) - } - return correlation.NewMap(correlation.MapUpdate{ - MultiKV: keyValues, - }) -} - func (TraceContext) GetAllKeys() []string { - return []string{TraceparentHeader, CorrelationContextHeader} + return []string{TraceparentHeader} } diff --git a/plugin/grpctrace/grpctrace.go b/plugin/grpctrace/grpctrace.go index 130d3eef882..b53713d6584 100644 --- a/plugin/grpctrace/grpctrace.go +++ b/plugin/grpctrace/grpctrace.go @@ -26,8 +26,12 @@ import ( ) var ( - propagator = trace.DefaultHTTPPropagator() - propagators = propagation.New(propagation.WithInjectors(propagator), propagation.WithExtractors(propagator)) + tcPropagator = trace.DefaultHTTPPropagator() + ccPropagator = correlation.DefaultHTTPPropagator() + propagators = propagation.New( + propagation.WithInjectors(tcPropagator, ccPropagator), + propagation.WithExtractors(tcPropagator, ccPropagator), + ) ) type metadataSupplier struct { diff --git a/plugin/httptrace/httptrace.go b/plugin/httptrace/httptrace.go index dda76c6a4d7..da91d36a2aa 100644 --- a/plugin/httptrace/httptrace.go +++ b/plugin/httptrace/httptrace.go @@ -29,8 +29,12 @@ var ( HostKey = key.New("http.host") URLKey = key.New("http.url") - propagator = trace.DefaultHTTPPropagator() - propagators = propagation.New(propagation.WithInjectors(propagator), propagation.WithExtractors(propagator)) + tcPropagator = trace.DefaultHTTPPropagator() + ccPropagator = correlation.DefaultHTTPPropagator() + propagators = propagation.New( + propagation.WithInjectors(tcPropagator, ccPropagator), + propagation.WithExtractors(tcPropagator, ccPropagator), + ) ) // Returns the Attributes, Context Entries, and SpanContext that were encoded by Inject. diff --git a/plugin/othttp/handler.go b/plugin/othttp/handler.go index 7a45e6d785f..65abf904cac 100644 --- a/plugin/othttp/handler.go +++ b/plugin/othttp/handler.go @@ -19,6 +19,7 @@ import ( "net/http" "go.opentelemetry.io/otel/api/core" + "go.opentelemetry.io/otel/api/correlation" "go.opentelemetry.io/otel/api/global" "go.opentelemetry.io/otel/api/propagation" "go.opentelemetry.io/otel/api/trace" @@ -128,10 +129,15 @@ func WithMessageEvents(events ...event) Option { // named after the operation and with any provided HandlerOptions. func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler { h := Handler{handler: handler, operation: operation} - propagator := trace.DefaultHTTPPropagator() + tcPropagator := trace.DefaultHTTPPropagator() + ccPropagator := correlation.DefaultHTTPPropagator() + props := propagation.New( + propagation.WithInjectors(tcPropagator, ccPropagator), + propagation.WithExtractors(tcPropagator, ccPropagator), + ) defaultOpts := []Option{ WithTracer(global.TraceProvider().Tracer("go.opentelemetry.io/plugin/othttp")), - WithPropagators(propagation.New(propagation.WithInjectors(propagator), propagation.WithExtractors(propagator))), + WithPropagators(props), WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)), }