diff --git a/api/correlation/context.go b/api/correlation/context.go index 5c7eba1cc20..edfa7d3fbea 100644 --- a/api/correlation/context.go +++ b/api/correlation/context.go @@ -1,3 +1,17 @@ +// Copyright 2020, 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 correlation import ( @@ -10,12 +24,13 @@ type correlationsType struct{} var correlationsKey = &correlationsType{} -// WithMap enters a Map into a new Context. +// WithMap returns a context with the Map entered into it. func WithMap(ctx context.Context, m Map) context.Context { return context.WithValue(ctx, correlationsKey, m) } -// WithMap enters a key:value set into a new Context. +// NewContext returns a context with the map from passed context +// updated with the passed key-value pairs. func NewContext(ctx context.Context, keyvalues ...core.KeyValue) context.Context { return WithMap(ctx, FromContext(ctx).Apply(MapUpdate{ MultiKV: keyvalues, diff --git a/api/correlation/doc.go b/api/correlation/doc.go new file mode 100644 index 00000000000..d6a9fe5fb05 --- /dev/null +++ b/api/correlation/doc.go @@ -0,0 +1,19 @@ +// Copyright 2020, 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. + +// This package implements the correlation functionality as specified +// in the OpenTelemetry specification. Currently it provides a data +// structure for storing correlations (Map) and a way of putting Map +// object into the context and retrieving it from context. +package correlation // import "go.opentelemetry.io/otel/api/correlation" diff --git a/api/correlation/map.go b/api/correlation/map.go index ec954bf95e8..13383f25332 100644 --- a/api/correlation/map.go +++ b/api/correlation/map.go @@ -1,4 +1,4 @@ -// Copyright 2019, OpenTelemetry Authors +// Copyright 2020, OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,21 +18,32 @@ import ( "go.opentelemetry.io/otel/api/core" ) -// TODO Comments needed! This was formerly known as distributedcontext.Map - -type entry struct { - value core.Value -} - -type rawMap map[core.Key]entry +type rawMap map[core.Key]core.Value +type keySet map[core.Key]struct{} +// Map is an immutable storage for correlations. type Map struct { m rawMap } +// MapUpdate contains information about correlation changes to be +// made. type MapUpdate struct { + // DropSingleK contains a single key to be dropped from + // correlations. Use this to avoid an overhead of a slice + // allocation if there is only one key to drop. + DropSingleK core.Key + // DropMultiK contains all the keys to be dropped from + // correlations. + DropMultiK []core.Key + + // SingleKV contains a single key-value pair to be added to + // correlations. Use this to avoid an overhead of a slice + // allocation if there is only one key-value pair to add. SingleKV core.KeyValue - MultiKV []core.KeyValue + // MultiKV contains all the key-value pairs to be added to + // correlations. + MultiKV []core.KeyValue } func newMap(raw rawMap) Map { @@ -41,28 +52,42 @@ func newMap(raw rawMap) Map { } } +// NewEmptyMap creates an empty correlations map. func NewEmptyMap() Map { return newMap(nil) } +// NewMap creates a map with the contents of the update applied. In +// this function, having an update with DropSingleK or DropMultiK +// makes no sense - those fields are effectively ignored. func NewMap(update MapUpdate) Map { return NewEmptyMap().Apply(update) } +// Apply creates a copy of the map with the contents of the update +// applied. Apply will first drop the keys from DropSingleK and +// DropMultiK, then add key-value pairs from SingleKV and MultiKV. func (m Map) Apply(update MapUpdate) Map { - r := make(rawMap, len(m.m)+len(update.MultiKV)) + delSet, addSet := getModificationSets(update) + mapSize := getNewMapSize(m.m, delSet, addSet) + + r := make(rawMap, mapSize) for k, v := range m.m { + // do not copy items we want to drop + if _, ok := delSet[k]; ok { + continue + } + // do not copy items we would overwrite + if _, ok := addSet[k]; ok { + continue + } r[k] = v } if update.SingleKV.Key.Defined() { - r[update.SingleKV.Key] = entry{ - value: update.SingleKV.Value, - } + r[update.SingleKV.Key] = update.SingleKV.Value } for _, kv := range update.MultiKV { - r[kv.Key] = entry{ - value: kv.Value, - } + r[kv.Key] = kv.Value } if len(r) == 0 { r = nil @@ -70,25 +95,82 @@ func (m Map) Apply(update MapUpdate) Map { return newMap(r) } +func getModificationSets(update MapUpdate) (delSet, addSet keySet) { + deletionsCount := len(update.DropMultiK) + if update.DropSingleK.Defined() { + deletionsCount++ + } + if deletionsCount > 0 { + delSet = make(map[core.Key]struct{}, deletionsCount) + for _, k := range update.DropMultiK { + delSet[k] = struct{}{} + } + if update.DropSingleK.Defined() { + delSet[update.DropSingleK] = struct{}{} + } + } + + additionsCount := len(update.MultiKV) + if update.SingleKV.Key.Defined() { + additionsCount++ + } + if additionsCount > 0 { + addSet = make(map[core.Key]struct{}, additionsCount) + for _, k := range update.MultiKV { + addSet[k.Key] = struct{}{} + } + if update.SingleKV.Key.Defined() { + addSet[update.SingleKV.Key] = struct{}{} + } + } + + return +} + +func getNewMapSize(m rawMap, delSet, addSet keySet) int { + mapSizeDiff := 0 + for k := range addSet { + if _, ok := m[k]; !ok { + mapSizeDiff++ + } + } + for k := range delSet { + if _, ok := m[k]; ok { + if _, inAddSet := addSet[k]; !inAddSet { + mapSizeDiff-- + } + } + } + return len(m) + mapSizeDiff +} + +// Value gets a value from correlations map and returns a boolean +// value indicating whether the key exist in the map. func (m Map) Value(k core.Key) (core.Value, bool) { - entry, ok := m.m[k] - return entry.value, ok + value, ok := m.m[k] + return value, ok } +// HasValue returns a boolean value indicating whether the key exist +// in the map. func (m Map) HasValue(k core.Key) bool { _, has := m.Value(k) return has } +// Len returns a length of the map. func (m Map) Len() int { return len(m.m) } +// Foreach calls a passed callback once on each key-value pair until +// all the key-value pairs of the map were iterated or the callback +// returns false, whichever happens first. func (m Map) Foreach(f func(kv core.KeyValue) bool) { for k, v := range m.m { if !f(core.KeyValue{ Key: k, - Value: v.value, + Value: v, }) { return } diff --git a/api/correlation/map_test.go b/api/correlation/map_test.go index 5161b17e5e9..8002a3f646b 100644 --- a/api/correlation/map_test.go +++ b/api/correlation/map_test.go @@ -1,4 +1,4 @@ -// Copyright 2019, OpenTelemetry Authors +// Copyright 2020, OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,15 +22,70 @@ import ( "go.opentelemetry.io/otel/api/key" ) +type testCase struct { + name string + value MapUpdate + init []int + wantKVs []core.KeyValue +} + func TestMap(t *testing.T) { - for _, testcase := range []struct { - name string - value MapUpdate - init []int - wantKVs []core.KeyValue - }{ - { - name: "NewMap with MultiKV", + for _, testcase := range getTestCases() { + t.Logf("Running test case %s", testcase.name) + var got Map + if len(testcase.init) > 0 { + got = makeTestMap(testcase.init).Apply(testcase.value) + } else { + got = NewMap(testcase.value) + } + for _, s := range testcase.wantKVs { + if ok := got.HasValue(s.Key); !ok { + t.Errorf("Expected Key %s to have Value", s.Key) + } + if g, ok := got.Value(s.Key); !ok || g != s.Value { + t.Errorf("+got: %v, -want: %v", g, s.Value) + } + } + // test Foreach() + got.Foreach(func(kv core.KeyValue) bool { + for _, want := range testcase.wantKVs { + if kv == want { + return false + } + } + t.Errorf("Expected kv %v, but not found", kv) + return true + }) + if l, exp := got.Len(), len(testcase.wantKVs); l != exp { + t.Errorf("+got: %d, -want: %d", l, exp) + } + } +} + +func TestSizeComputation(t *testing.T) { + for _, testcase := range getTestCases() { + t.Logf("Running test case %s", testcase.name) + var initMap Map + if len(testcase.init) > 0 { + initMap = makeTestMap(testcase.init) + } else { + initMap = NewEmptyMap() + } + gotMap := initMap.Apply(testcase.value) + + delSet, addSet := getModificationSets(testcase.value) + mapSize := getNewMapSize(initMap.m, delSet, addSet) + + if gotMap.Len() != mapSize { + t.Errorf("Expected computed size to be %d, got %d", gotMap.Len(), mapSize) + } + } +} + +func getTestCases() []testCase { + return []testCase{ + { + name: "New map with MultiKV", value: MapUpdate{MultiKV: []core.KeyValue{ key.Int64("key1", 1), key.String("key2", "val2")}, @@ -42,7 +97,7 @@ func TestMap(t *testing.T) { }, }, { - name: "NewMap with SingleKV", + name: "New map with SingleKV", value: MapUpdate{SingleKV: key.String("key1", "val1")}, init: []int{}, wantKVs: []core.KeyValue{ @@ -50,7 +105,7 @@ func TestMap(t *testing.T) { }, }, { - name: "NewMap with MapUpdate", + name: "New map with both add fields", value: MapUpdate{SingleKV: key.Int64("key1", 3), MultiKV: []core.KeyValue{ key.String("key1", ""), @@ -63,13 +118,62 @@ func TestMap(t *testing.T) { }, }, { - name: "NewMap with empty MapUpdate", - value: MapUpdate{MultiKV: []core.KeyValue{}}, + name: "New map with empty MapUpdate", + value: MapUpdate{}, + init: []int{}, + wantKVs: []core.KeyValue{}, + }, + { + name: "New map with DropSingleK", + value: MapUpdate{DropSingleK: core.Key("key1")}, + init: []int{}, + wantKVs: []core.KeyValue{}, + }, + { + name: "New map with DropMultiK", + value: MapUpdate{DropMultiK: []core.Key{ + core.Key("key1"), core.Key("key2"), + }}, init: []int{}, wantKVs: []core.KeyValue{}, }, { - name: "Map with MultiKV", + name: "New map with both drop fields", + value: MapUpdate{ + DropSingleK: core.Key("key1"), + DropMultiK: []core.Key{ + core.Key("key1"), + core.Key("key2"), + }, + }, + init: []int{}, + wantKVs: []core.KeyValue{}, + }, + { + name: "New map with all fields", + value: MapUpdate{ + DropSingleK: core.Key("key1"), + DropMultiK: []core.Key{ + core.Key("key1"), + core.Key("key2"), + }, + SingleKV: key.String("key4", "val4"), + MultiKV: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + key.String("key3", "val3"), + }, + }, + init: []int{}, + wantKVs: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + key.String("key3", "val3"), + key.String("key4", "val4"), + }, + }, + { + name: "Existing map with MultiKV", value: MapUpdate{MultiKV: []core.KeyValue{ key.Int64("key1", 1), key.String("key2", "val2")}, @@ -82,7 +186,7 @@ func TestMap(t *testing.T) { }, }, { - name: "Map with SingleKV", + name: "Existing map with SingleKV", value: MapUpdate{SingleKV: key.String("key1", "val1")}, init: []int{5}, wantKVs: []core.KeyValue{ @@ -91,7 +195,7 @@ func TestMap(t *testing.T) { }, }, { - name: "Map with MapUpdate", + name: "Existing map with both add fields", value: MapUpdate{SingleKV: key.Int64("key1", 3), MultiKV: []core.KeyValue{ key.String("key1", ""), @@ -105,51 +209,78 @@ func TestMap(t *testing.T) { }, }, { - name: "Map with empty MapUpdate", - value: MapUpdate{MultiKV: []core.KeyValue{}}, + name: "Existing map with empty MapUpdate", + value: MapUpdate{}, init: []int{5}, wantKVs: []core.KeyValue{ key.Int("key5", 5), }, }, - } { - t.Logf("Running test case %s", testcase.name) - var got Map - if len(testcase.init) > 0 { - got = makeTestMap(testcase.init).Apply(testcase.value) - } else { - got = NewMap(testcase.value) - } - for _, s := range testcase.wantKVs { - if ok := got.HasValue(s.Key); !ok { - t.Errorf("Expected Key %s to have Value", s.Key) - } - if g, ok := got.Value(s.Key); !ok || g != s.Value { - t.Errorf("+got: %v, -want: %v", g, s.Value) - } - } - // test Foreach() - got.Foreach(func(kv core.KeyValue) bool { - for _, want := range testcase.wantKVs { - if kv == want { - return false - } - } - t.Errorf("Expected kv %v, but not found", kv) - return true - }) - if l, exp := got.Len(), len(testcase.wantKVs); l != exp { - t.Errorf("+got: %d, -want: %d", l, exp) - } + { + name: "Existing map with DropSingleK", + value: MapUpdate{DropSingleK: core.Key("key1")}, + init: []int{1, 5}, + wantKVs: []core.KeyValue{ + key.Int("key5", 5), + }, + }, + { + name: "Existing map with DropMultiK", + value: MapUpdate{DropMultiK: []core.Key{ + core.Key("key1"), core.Key("key2"), + }}, + init: []int{1, 5}, + wantKVs: []core.KeyValue{ + key.Int("key5", 5), + }, + }, + { + name: "Existing map with both drop fields", + value: MapUpdate{ + DropSingleK: core.Key("key1"), + DropMultiK: []core.Key{ + core.Key("key1"), + core.Key("key2"), + }, + }, + init: []int{1, 2, 5}, + wantKVs: []core.KeyValue{ + key.Int("key5", 5), + }, + }, + { + name: "Existing map with all the fields", + value: MapUpdate{ + DropSingleK: core.Key("key1"), + DropMultiK: []core.Key{ + core.Key("key1"), + core.Key("key2"), + core.Key("key5"), + core.Key("key6"), + }, + SingleKV: key.String("key4", "val4"), + MultiKV: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + key.String("key3", "val3"), + }, + }, + init: []int{5, 6, 7}, + wantKVs: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + key.String("key3", "val3"), + key.String("key4", "val4"), + key.Int("key7", 7), + }, + }, } } func makeTestMap(ints []int) Map { r := make(rawMap, len(ints)) for _, v := range ints { - r[core.Key(fmt.Sprintf("key%d", v))] = entry{ - value: core.Int(v), - } + r[core.Key(fmt.Sprintf("key%d", v))] = core.Int(v) } return newMap(r) }