diff --git a/api/correlation/context.go b/api/correlation/context.go index 5c7eba1cc..edfa7d3fb 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 000000000..d6a9fe5fb --- /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 ec954bf95..13383f253 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 (m Map) Value(k core.Key) (core.Value, bool) { - entry, ok := m.m[k] - return entry.value, ok +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) { + 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 5161b17e5..8002a3f64 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,97 +22,15 @@ 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", - value: MapUpdate{MultiKV: []core.KeyValue{ - key.Int64("key1", 1), - key.String("key2", "val2")}, - }, - init: []int{}, - wantKVs: []core.KeyValue{ - key.Int64("key1", 1), - key.String("key2", "val2"), - }, - }, - { - name: "NewMap with SingleKV", - value: MapUpdate{SingleKV: key.String("key1", "val1")}, - init: []int{}, - wantKVs: []core.KeyValue{ - key.String("key1", "val1"), - }, - }, - { - name: "NewMap with MapUpdate", - value: MapUpdate{SingleKV: key.Int64("key1", 3), - MultiKV: []core.KeyValue{ - key.String("key1", ""), - key.String("key2", "val2")}, - }, - init: []int{}, - wantKVs: []core.KeyValue{ - key.String("key1", ""), - key.String("key2", "val2"), - }, - }, - { - name: "NewMap with empty MapUpdate", - value: MapUpdate{MultiKV: []core.KeyValue{}}, - init: []int{}, - wantKVs: []core.KeyValue{}, - }, - { - name: "Map with MultiKV", - value: MapUpdate{MultiKV: []core.KeyValue{ - key.Int64("key1", 1), - key.String("key2", "val2")}, - }, - init: []int{5}, - wantKVs: []core.KeyValue{ - key.Int64("key1", 1), - key.String("key2", "val2"), - key.Int("key5", 5), - }, - }, - { - name: "Map with SingleKV", - value: MapUpdate{SingleKV: key.String("key1", "val1")}, - init: []int{5}, - wantKVs: []core.KeyValue{ - key.String("key1", "val1"), - key.Int("key5", 5), - }, - }, - { - name: "Map with MapUpdate", - value: MapUpdate{SingleKV: key.Int64("key1", 3), - MultiKV: []core.KeyValue{ - key.String("key1", ""), - key.String("key2", "val2")}, - }, - init: []int{5}, - wantKVs: []core.KeyValue{ - key.String("key1", ""), - key.String("key2", "val2"), - key.Int("key5", 5), - }, - }, - { - name: "Map with empty MapUpdate", - value: MapUpdate{MultiKV: []core.KeyValue{}}, - init: []int{5}, - wantKVs: []core.KeyValue{ - key.Int("key5", 5), - }, - }, - } { + for _, testcase := range getTestCases() { t.Logf("Running test case %s", testcase.name) var got Map if len(testcase.init) > 0 { @@ -144,12 +62,225 @@ func TestMap(t *testing.T) { } } +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")}, + }, + init: []int{}, + wantKVs: []core.KeyValue{ + key.Int64("key1", 1), + key.String("key2", "val2"), + }, + }, + { + name: "New map with SingleKV", + value: MapUpdate{SingleKV: key.String("key1", "val1")}, + init: []int{}, + wantKVs: []core.KeyValue{ + key.String("key1", "val1"), + }, + }, + { + name: "New map with both add fields", + value: MapUpdate{SingleKV: key.Int64("key1", 3), + MultiKV: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2")}, + }, + init: []int{}, + wantKVs: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + }, + }, + { + 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: "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")}, + }, + init: []int{5}, + wantKVs: []core.KeyValue{ + key.Int64("key1", 1), + key.String("key2", "val2"), + key.Int("key5", 5), + }, + }, + { + name: "Existing map with SingleKV", + value: MapUpdate{SingleKV: key.String("key1", "val1")}, + init: []int{5}, + wantKVs: []core.KeyValue{ + key.String("key1", "val1"), + key.Int("key5", 5), + }, + }, + { + name: "Existing map with both add fields", + value: MapUpdate{SingleKV: key.Int64("key1", 3), + MultiKV: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2")}, + }, + init: []int{5}, + wantKVs: []core.KeyValue{ + key.String("key1", ""), + key.String("key2", "val2"), + key.Int("key5", 5), + }, + }, + { + name: "Existing map with empty MapUpdate", + value: MapUpdate{}, + init: []int{5}, + wantKVs: []core.KeyValue{ + key.Int("key5", 5), + }, + }, + { + 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) }