Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Context Propagation @ OTEP 66 #381

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/distributedcontext/map.go → api/context/baggage/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package distributedcontext
package baggage

import (
"go.opentelemetry.io/otel/api/core"
)

// TODO Comments needed! This was formerly known as distributedcontext.Map

type entry struct {
value core.Value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package distributedcontext
package baggage

import (
"fmt"
Expand Down
117 changes: 117 additions & 0 deletions api/context/baggage/propagation/propagation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package propagation

import (
"context"
"net/url"
"strings"

"go.opentelemetry.io/otel/api/context/baggage"
"go.opentelemetry.io/otel/api/context/propagation"
"go.opentelemetry.io/otel/api/core"
"go.opentelemetry.io/otel/api/key"
)

type correlationsType struct{}

var (
// CorrelationContextHeader is specified by W3C.
CorrelationContextHeader = "Correlation-Context"

correlationsKey = &correlationsType{}
)

// CorrelationContext propagates Key:Values in W3C TraceContext format.
type CorrelationContext struct{}

var _ propagation.HTTPPropagator = CorrelationContext{}

// WithMap enters a baggage.Map into a new Context.
func WithMap(ctx context.Context, m baggage.Map) context.Context {
return context.WithValue(ctx, correlationsKey, m)
}

// WithMap enters a key:value set into a new Context.
func NewContext(ctx context.Context, keyvalues ...core.KeyValue) context.Context {
return WithMap(ctx, FromContext(ctx).Apply(baggage.MapUpdate{
MultiKV: keyvalues,
}))
}

// FromContext gets the current baggage.Map from a Context.
func FromContext(ctx context.Context) baggage.Map {
if m, ok := ctx.Value(correlationsKey).(baggage.Map); ok {
return m
}
return baggage.NewEmptyMap()
}

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

// Inject implements HTTPExtractor.
func (CorrelationContext) Extract(ctx context.Context, supplier propagation.HTTPSupplier) context.Context {
correlationContext := supplier.Get(CorrelationContextHeader)
if correlationContext == "" {
return WithMap(ctx, baggage.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, baggage.NewMap(baggage.MapUpdate{
MultiKV: keyValues,
}))
}

// GetAllKeys implements HTTPPropagator.
func (CorrelationContext) GetAllKeys() []string {
return []string{CorrelationContextHeader}
}
215 changes: 215 additions & 0 deletions api/context/baggage/propagation/propagation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package propagation_test

import (
"context"
"net/http"
"strings"
"testing"

"github.com/google/go-cmp/cmp"

"go.opentelemetry.io/otel/api/context/baggage"
bpropagation "go.opentelemetry.io/otel/api/context/baggage/propagation"
"go.opentelemetry.io/otel/api/context/propagation"
"go.opentelemetry.io/otel/api/core"
"go.opentelemetry.io/otel/api/key"
)

func TestExtractValidDistributedContextFromHTTPReq(t *testing.T) {
props := propagation.New(propagation.WithExtractors(bpropagation.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 := bpropagation.FromContext(ctx)
wantCorCtx := baggage.NewMap(baggage.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(bpropagation.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 := bpropagation.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) {
props := propagation.New(propagation.WithInjectors(bpropagation.CorrelationContext{}))
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 := bpropagation.WithMap(context.Background(), baggage.NewMap(baggage.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 bpropagation.CorrelationContext
want := []string{"Correlation-Context"}
got := propagator.GetAllKeys()
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("GetAllKeys: -got +want %s", diff)
}
}
Loading