diff --git a/api/core/span_context.go b/api/core/span_context.go index 8a4d9827abb..b6e4c1aefa6 100644 --- a/api/core/span_context.go +++ b/api/core/span_context.go @@ -36,6 +36,7 @@ type SpanContext struct { TraceID TraceID SpanID uint64 TraceOptions byte + Tracestate *Tracestate } var ( diff --git a/api/core/tracestate.go b/api/core/tracestate.go new file mode 100644 index 00000000000..57b344a15cf --- /dev/null +++ b/api/core/tracestate.go @@ -0,0 +1,140 @@ +// 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 tracestate implements support for the Tracestate header of the +// W3C TraceContext propagation format. +package core + +import ( + "fmt" + "regexp" +) + +const ( + keyMaxSize = 256 + valueMaxSize = 256 + maxKeyValuePairs = 32 +) + +const ( + keyWithoutVendorFormat = `[a-z][_0-9a-z\-\*\/]{0,255}` + keyWithVendorFormat = `[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}` + keyFormat = `(` + keyWithoutVendorFormat + `)|(` + keyWithVendorFormat + `)` + valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]` +) + +var keyValidationRegExp = regexp.MustCompile(`^(` + keyFormat + `)$`) +var valueValidationRegExp = regexp.MustCompile(`^(` + valueFormat + `)$`) + +// Tracestate represents tracing-system specific context in a list of key-value pairs. Tracestate allows different +// vendors propagate additional information and inter-operate with their legacy Id formats. +type Tracestate struct { + // Key is an opaque string up to 256 characters printable. It MUST begin with a lowercase letter, + // and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and + // forward slashes /. + // Value is an opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the + // range 0x20 to 0x7E) except comma , and =. + entries []KeyValue +} + +// Entries returns a slice of core.KeyValues. +func (ts *Tracestate) Entries() []KeyValue { + if ts == nil { + return nil + } + return ts.entries +} + +func (ts *Tracestate) remove(key string) *KeyValue { + for index, entry := range ts.entries { + if entry.Key.Variable.Name == key { + ts.entries = append(ts.entries[:index], ts.entries[index+1:]...) + return &entry + } + } + return nil +} + +func (ts *Tracestate) add(entries []KeyValue) error { + for _, entry := range entries { + ts.remove(entry.Key.Variable.Name) + } + if len(ts.entries)+len(entries) > maxKeyValuePairs { + return fmt.Errorf("adding %d key-value pairs to current %d pairs exceeds the limit of %d", + len(entries), len(ts.entries), maxKeyValuePairs) + } + ts.entries = append(entries, ts.entries...) + return nil +} + +func isValid(entry KeyValue) bool { + return keyValidationRegExp.MatchString(entry.Key.Variable.Name) && + valueValidationRegExp.MatchString(entry.Value.String) +} + +func containsDuplicateKey(entries ...KeyValue) (string, bool) { + keyMap := make(map[string]int) + for _, entry := range entries { + if _, ok := keyMap[entry.Key.Variable.Name]; ok { + return entry.Key.Variable.Name, true + } + keyMap[entry.Key.Variable.Name] = 1 + } + return "", false +} + +func areEntriesValid(entries ...KeyValue) (*KeyValue, bool) { + for _, entry := range entries { + if !isValid(entry) { + return &entry, false + } + } + return nil, true +} + +// New creates a Tracestate object from a parent and/or entries (key-value pair). +// Entries from the parent are copied if present. The entries passed to this function +// are inserted in front of those copied from the parent. If an entry copied from the +// parent contains the same key as one of the entry in entries then the entry copied +// from the parent is removed. See add func. +// +// An error is returned with nil Tracestate if +// 1. one or more entry in entries is invalid. +// 2. two or more entries in the input entries have the same key. +// 3. the number of entries combined from the parent and the input entries exceeds maxKeyValuePairs. +// (duplicate entry is counted only once). +func New(parent *Tracestate, entries ...KeyValue) (*Tracestate, error) { + if parent == nil && len(entries) == 0 { + return nil, nil + } + if entry, ok := areEntriesValid(entries...); !ok { + return nil, fmt.Errorf("key-value pair {%s, %s} is invalid", entry.Key.Variable.Name, entry.Value.String) + } + + if key, duplicate := containsDuplicateKey(entries...); duplicate { + return nil, fmt.Errorf("contains duplicate keys (%s)", key) + } + + tracestate := Tracestate{} + + if parent != nil && len(parent.entries) > 0 { + tracestate.entries = append([]KeyValue{}, parent.entries...) + } + + err := tracestate.add(entries) + if err != nil { + return nil, err + } + return &tracestate, nil +} diff --git a/api/core/tracestate_test.go b/api/core/tracestate_test.go new file mode 100644 index 00000000000..25b7a8061a6 --- /dev/null +++ b/api/core/tracestate_test.go @@ -0,0 +1,324 @@ +// 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 core + +import ( + "fmt" + "testing" + + "go.opentelemetry.io/api/registry" +) + +func checkFront(t *testing.T, tracestate *Tracestate, wantKey, testname string) { + gotKey := tracestate.entries[0].Key.Variable.Name + if gotKey != wantKey { + t.Errorf("test:%s: first entry in the list: got %q want %q", testname, gotKey, wantKey) + } +} + +func checkBack(t *testing.T, tracestate *Tracestate, wantKey, testname string) { + gotKey := tracestate.entries[len(tracestate.entries)-1].Key.Variable.Name + if gotKey != wantKey { + t.Errorf("test:%s: last entry in the list: got %q want %q", testname, gotKey, wantKey) + } +} + +func checkSize(t *testing.T, tracestate *Tracestate, wantSize int, testname string) { + if gotSize := len(tracestate.entries); gotSize != wantSize { + t.Errorf("test:%s: size of the list: got %q want %q", testname, gotSize, wantSize) + } +} + +func (ts *Tracestate) get(key string) (string, bool) { + if ts == nil { + return "", false + } + for _, entry := range ts.entries { + if entry.Key.Variable.Name == key { + return entry.Value.String, true + } + } + return "", false +} + +func checkKeyValue(t *testing.T, tracestate *Tracestate, key, wantValue, testname string) { + wantOk := true + if wantValue == "" { + wantOk = false + } + gotValue, gotOk := tracestate.get(key) + if wantOk != gotOk || gotValue != wantValue { + t.Errorf("test:%s: get value for key=%s failed: got %q want %q", testname, key, gotValue, wantValue) + } +} + +func checkError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) { + if err != nil { + t.Errorf("test:%s: %s: tracestate=%v, error= %v", testname, msg, tracestate, err) + } +} + +func wantError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) { + if err == nil { + t.Errorf("test:%s: %s: tracestate=%v, error=%v", testname, msg, tracestate, err) + } +} + +func TestCreateWithNullParent(t *testing.T) { + key1, value1 := "hello", "world" + testname := "TestCreateWithNullParent" + + entry := createKeyValue(key1, value1) + tracestate, err := New(nil, entry) + checkError(t, tracestate, err, testname, "create failed from null parent") + checkKeyValue(t, tracestate, key1, value1, testname) +} + +func TestCreateFromParentWithSingleKey(t *testing.T) { + key1, value1, key2, value2 := "hello", "world", "foo", "bar" + testname := "TestCreateFromParentWithSingleKey" + + entry1 := createKeyValue(key1, value1) + entry2 := createKeyValue(key2, value2) + parent, _ := New(nil, entry1) + tracestate, err := New(parent, entry2) + + checkError(t, tracestate, err, testname, "create failed from parent with single key") + checkKeyValue(t, tracestate, key2, value2, testname) + checkFront(t, tracestate, key2, testname) + checkBack(t, tracestate, key1, testname) +} + +func TestCreateFromParentWithDoubleKeys(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "bar", "baz" + testname := "TestCreateFromParentWithDoubleKeys" + + entry1 := createKeyValue(key1, value1) + entry2 := createKeyValue(key2, value2) + entry3 := createKeyValue(key3, value3) + parent, _ := New(nil, entry2, entry1) + tracestate, err := New(parent, entry3) + + checkError(t, tracestate, err, testname, "create failed from parent with double keys") + checkKeyValue(t, tracestate, key3, value3, testname) + checkFront(t, tracestate, key3, testname) + checkBack(t, tracestate, key1, testname) +} + +func TestCreateFromParentWithExistingKey(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz" + testname := "TestCreateFromParentWithExistingKey" + + entry1 := createKeyValue(key1, value1) + entry2 := createKeyValue(key2, value2) + entry3 := createKeyValue(key3, value3) + parent, _ := New(nil, entry2, entry1) + tracestate, err := New(parent, entry3) + + checkError(t, tracestate, err, testname, "create failed with an existing key") + checkKeyValue(t, tracestate, key3, value3, testname) + checkFront(t, tracestate, key3, testname) + checkBack(t, tracestate, key2, testname) + checkSize(t, tracestate, 2, testname) +} + +func TestImplicitImmutableTracestate(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "hello", "bar", "foo", "baz" + testname := "TestImplicitImmutableTracestate" + + entry1 := createKeyValue(key1, value1) + entry2 := createKeyValue(key2, value2) + parent, _ := New(nil, entry1) + tracestate, err := New(parent, entry2) + + checkError(t, tracestate, err, testname, "create failed") + checkKeyValue(t, tracestate, key2, value2, testname) + checkKeyValue(t, parent, key2, value1, testname) + + // Get and update entries. + entries := tracestate.Entries() + entry := createKeyValue(key3, value3) + entries = append(entries, entry) + + // Check Tracestate does not have key3. + checkKeyValue(t, tracestate, key3, "", testname) + // Check that we added the key3 in the entries + tracestate, err = New(nil, entries...) + checkError(t, tracestate, err, testname, "create failed") + checkKeyValue(t, tracestate, key3, value3, testname) +} + +func TestKeyWithValidChar(t *testing.T) { + testname := "TestKeyWithValidChar" + + arrayRune := []rune("") + for c := 'a'; c <= 'z'; c++ { + arrayRune = append(arrayRune, c) + } + for c := '0'; c <= '9'; c++ { + arrayRune = append(arrayRune, c) + } + arrayRune = append(arrayRune, '_') + arrayRune = append(arrayRune, '-') + arrayRune = append(arrayRune, '*') + arrayRune = append(arrayRune, '/') + key := string(arrayRune) + entry := createKeyValue(key, "world") + tracestate, err := New(nil, entry) + + checkError(t, tracestate, err, testname, "create failed when the key contains all valid characters") +} + +func TestKeyWithInvalidChar(t *testing.T) { + testname := "TestKeyWithInvalidChar" + + keys := []string{"1ab", "1ab2", "Abc", " abc", "a=b"} + + for _, key := range keys { + entry := createKeyValue(key, "world") + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, fmt.Sprintf( + "create did not err with invalid key=%q", key)) + } +} + +func TestNilKey(t *testing.T) { + testname := "TestNilKey" + + entry := createKeyValue("", "world") + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, "create did not err when the key is nil (\"\")") +} + +func TestValueWithInvalidChar(t *testing.T) { + testname := "TestValueWithInvalidChar" + + keys := []string{"A=B", "A,B", "AB "} + + for _, value := range keys { + entry := createKeyValue("hello", value) + tracestate, err := New(nil, entry) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the value is invalid (%q)", value)) + } +} + +func TestNilValue(t *testing.T) { + testname := "TestNilValue" + + tracestate, err := New(nil, createKeyValue("hello", "")) + wantError(t, tracestate, err, testname, "create did not err when the value is nil (\"\")") +} + +func TestInvalidKeyLen(t *testing.T) { + testname := "TestInvalidKeyLen" + + arrayRune := []rune("") + for i := 0; i <= keyMaxSize+1; i++ { + arrayRune = append(arrayRune, 'a') + } + key := string(arrayRune) + tracestate, err := New(nil, createKeyValue(key, "world")) + + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the length (%d) of the key is larger than max (%d)", + len(key), keyMaxSize)) +} + +func TestInvalidValueLen(t *testing.T) { + testname := "TestInvalidValueLen" + + arrayRune := []rune("") + for i := 0; i <= valueMaxSize+1; i++ { + arrayRune = append(arrayRune, 'a') + } + value := string(arrayRune) + tracestate, err := New(nil, createKeyValue("hello", value)) + + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the length (%d) of the value is larger than max (%d)", + len(value), valueMaxSize)) +} + +func TestCreateFromArrayWithOverLimitKVPairs(t *testing.T) { + testname := "TestCreateFromArrayWithOverLimitKVPairs" + + entries := []KeyValue{} + for i := 0; i <= maxKeyValuePairs; i++ { + key := fmt.Sprintf("a%db", i) + entry := createKeyValue(key, "world") + entries = append(entries, entry) + } + tracestate, err := New(nil, entries...) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when the number (%d) of key-value pairs is larger than max (%d)", + len(entries), maxKeyValuePairs)) +} + +func TestCreateFromEmptyArray(t *testing.T) { + testname := "TestCreateFromEmptyArray" + + tracestate, err := New(nil, nil...) + checkError(t, tracestate, err, testname, + "failed to create nil tracestate") +} + +func TestCreateFromParentWithOverLimitKVPairs(t *testing.T) { + testname := "TestCreateFromParentWithOverLimitKVPairs" + + entries := []KeyValue{} + for i := 0; i < maxKeyValuePairs; i++ { + key := fmt.Sprintf("a%db", i) + entry := createKeyValue(key, "world") + entries = append(entries, entry) + } + parent, err := New(nil, entries...) + + checkError(t, parent, err, testname, fmt.Sprintf("create failed to add %d key-value pair", maxKeyValuePairs)) + + // Add one more to go over the limit + key := fmt.Sprintf("a%d", maxKeyValuePairs) + tracestate, err := New(parent, createKeyValue(key, "world")) + wantError(t, tracestate, err, testname, + fmt.Sprintf("create did not err when attempted to exceed the key-value pair limit of %d", maxKeyValuePairs)) +} + +func TestCreateFromArrayWithDuplicateKeys(t *testing.T) { + key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz" + testname := "TestCreateFromArrayWithDuplicateKeys" + + entry1 := createKeyValue(key1, value1) + entry2 := createKeyValue(key2, value2) + entry3 := createKeyValue(key3, value3) + tracestate, err := New(nil, entry1, entry2, entry3) + + wantError(t, tracestate, err, testname, + "create did not err when entries contained duplicate keys") +} + +func TestEntriesWithNil(t *testing.T) { + ts, err := New(nil) + if err != nil { + t.Fatal(err) + } + if got, want := len(ts.Entries()), 0; got != want { + t.Errorf("zero value should have no entries, got %v; want %v", got, want) + } +} + +func createKeyValue(key, value string) KeyValue { + return KeyValue{Key{registry.Variable{Name: key}}, Value{Type: STRING, String: value}} + +}