1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-08-10 22:31:50 +02:00

Rename otel/label -> otel/attribute (#1541)

* Rename otel/label -> otel/attr

Leave the imported name alone, to avoid a large diff and conflicts

* Better import comment

* Update CHANGELOG.md

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* otel/attr -> otel/attribute

* Missed the changelog entry

* Get rid of import renaming

* Merge remaining conflicts

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
This commit is contained in:
Punya Biswal
2021-02-18 12:59:37 -05:00
committed by GitHub
parent 1b5b662136
commit ecf65d7968
123 changed files with 1715 additions and 1715 deletions

220
attribute/benchmark_test.go Normal file
View File

@@ -0,0 +1,220 @@
// Copyright The 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 attribute_test
import (
"testing"
"go.opentelemetry.io/otel/attribute"
)
type test struct{}
var (
arrayVal = []string{"one", "two"}
arrayKeyVal = attribute.Array("array", arrayVal)
boolVal = true
boolKeyVal = attribute.Bool("bool", boolVal)
intVal = int(1)
intKeyVal = attribute.Int("int", intVal)
int8Val = int8(1)
int8KeyVal = attribute.Int("int8", int(int8Val))
int16Val = int16(1)
int16KeyVal = attribute.Int("int16", int(int16Val))
int64Val = int64(1)
int64KeyVal = attribute.Int64("int64", int64Val)
float64Val = float64(1.0)
float64KeyVal = attribute.Float64("float64", float64Val)
stringVal = "string"
stringKeyVal = attribute.String("string", stringVal)
bytesVal = []byte("bytes")
structVal = test{}
)
func BenchmarkArrayKey(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Array("array", arrayVal)
}
}
func BenchmarkArrayKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("array", arrayVal)
}
}
func BenchmarkBoolKey(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Bool("bool", boolVal)
}
}
func BenchmarkBoolKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("bool", boolVal)
}
}
func BenchmarkIntKey(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Int("int", intVal)
}
}
func BenchmarkIntKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("int", intVal)
}
}
func BenchmarkInt8KeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("int8", int8Val)
}
}
func BenchmarkInt16KeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("int16", int16Val)
}
}
func BenchmarkInt64Key(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Int64("int64", int64Val)
}
}
func BenchmarkInt64KeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("int64", int64Val)
}
}
func BenchmarkFloat64Key(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Float64("float64", float64Val)
}
}
func BenchmarkFloat64KeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("float64", float64Val)
}
}
func BenchmarkStringKey(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.String("string", stringVal)
}
}
func BenchmarkStringKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("string", stringVal)
}
}
func BenchmarkBytesKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("bytes", bytesVal)
}
}
func BenchmarkStructKeyAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = attribute.Any("struct", structVal)
}
}
func BenchmarkEmitArray(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = arrayKeyVal.Value.Emit()
}
}
func BenchmarkEmitBool(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = boolKeyVal.Value.Emit()
}
}
func BenchmarkEmitInt(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = intKeyVal.Value.Emit()
}
}
func BenchmarkEmitInt8(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = int8KeyVal.Value.Emit()
}
}
func BenchmarkEmitInt16(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = int16KeyVal.Value.Emit()
}
}
func BenchmarkEmitInt64(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = int64KeyVal.Value.Emit()
}
}
func BenchmarkEmitFloat64(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = float64KeyVal.Value.Emit()
}
}
func BenchmarkEmitString(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = stringKeyVal.Value.Emit()
}
}

20
attribute/doc.go Normal file
View File

@@ -0,0 +1,20 @@
// Copyright The 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 attribute provides key and value attributes.
//
// This package is currently in a pre-GA phase. Backwards incompatible changes
// may be introduced in subsequent minor version releases as we work to track
// the evolving OpenTelemetry specification and user feedback.
package attribute // import "go.opentelemetry.io/otel/attribute"

150
attribute/encoder.go Normal file
View File

@@ -0,0 +1,150 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
import (
"bytes"
"sync"
"sync/atomic"
)
type (
// Encoder is a mechanism for serializing a label set into a
// specific string representation that supports caching, to
// avoid repeated serialization. An example could be an
// exporter encoding the label set into a wire representation.
Encoder interface {
// Encode returns the serialized encoding of the label
// set using its Iterator. This result may be cached
// by a attribute.Set.
Encode(iterator Iterator) string
// ID returns a value that is unique for each class of
// label encoder. Label encoders allocate these using
// `NewEncoderID`.
ID() EncoderID
}
// EncoderID is used to identify distinct Encoder
// implementations, for caching encoded results.
EncoderID struct {
value uint64
}
// defaultLabelEncoder uses a sync.Pool of buffers to reduce
// the number of allocations used in encoding labels. This
// implementation encodes a comma-separated list of key=value,
// with '/'-escaping of '=', ',', and '\'.
defaultLabelEncoder struct {
// pool is a pool of labelset builders. The buffers in this
// pool grow to a size that most label encodings will not
// allocate new memory.
pool sync.Pool // *bytes.Buffer
}
)
// escapeChar is used to ensure uniqueness of the label encoding where
// keys or values contain either '=' or ','. Since there is no parser
// needed for this encoding and its only requirement is to be unique,
// this choice is arbitrary. Users will see these in some exporters
// (e.g., stdout), so the backslash ('\') is used as a conventional choice.
const escapeChar = '\\'
var (
_ Encoder = &defaultLabelEncoder{}
// encoderIDCounter is for generating IDs for other label
// encoders.
encoderIDCounter uint64
defaultEncoderOnce sync.Once
defaultEncoderID = NewEncoderID()
defaultEncoderInstance *defaultLabelEncoder
)
// NewEncoderID returns a unique label encoder ID. It should be
// called once per each type of label encoder. Preferably in init() or
// in var definition.
func NewEncoderID() EncoderID {
return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)}
}
// DefaultEncoder returns a label encoder that encodes labels
// in such a way that each escaped label's key is followed by an equal
// sign and then by an escaped label's value. All key-value pairs are
// separated by a comma.
//
// Escaping is done by prepending a backslash before either a
// backslash, equal sign or a comma.
func DefaultEncoder() Encoder {
defaultEncoderOnce.Do(func() {
defaultEncoderInstance = &defaultLabelEncoder{
pool: sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
},
}
})
return defaultEncoderInstance
}
// Encode is a part of an implementation of the LabelEncoder
// interface.
func (d *defaultLabelEncoder) Encode(iter Iterator) string {
buf := d.pool.Get().(*bytes.Buffer)
defer d.pool.Put(buf)
buf.Reset()
for iter.Next() {
i, keyValue := iter.IndexedLabel()
if i > 0 {
_, _ = buf.WriteRune(',')
}
copyAndEscape(buf, string(keyValue.Key))
_, _ = buf.WriteRune('=')
if keyValue.Value.Type() == STRING {
copyAndEscape(buf, keyValue.Value.AsString())
} else {
_, _ = buf.WriteString(keyValue.Value.Emit())
}
}
return buf.String()
}
// ID is a part of an implementation of the LabelEncoder interface.
func (*defaultLabelEncoder) ID() EncoderID {
return defaultEncoderID
}
// copyAndEscape escapes `=`, `,` and its own escape character (`\`),
// making the default encoding unique.
func copyAndEscape(buf *bytes.Buffer, val string) {
for _, ch := range val {
switch ch {
case '=', ',', escapeChar:
buf.WriteRune(escapeChar)
}
buf.WriteRune(ch)
}
}
// Valid returns true if this encoder ID was allocated by
// `NewEncoderID`. Invalid encoder IDs will not be cached.
func (id EncoderID) Valid() bool {
return id.value != 0
}

143
attribute/iterator.go Normal file
View File

@@ -0,0 +1,143 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
// Iterator allows iterating over the set of labels in order,
// sorted by key.
type Iterator struct {
storage *Set
idx int
}
// MergeIterator supports iterating over two sets of labels while
// eliminating duplicate values from the combined set. The first
// iterator value takes precedence.
type MergeIterator struct {
one oneIterator
two oneIterator
current KeyValue
}
type oneIterator struct {
iter Iterator
done bool
label KeyValue
}
// Next moves the iterator to the next position. Returns false if there
// are no more labels.
func (i *Iterator) Next() bool {
i.idx++
return i.idx < i.Len()
}
// Label returns current KeyValue. Must be called only after Next returns
// true.
func (i *Iterator) Label() KeyValue {
kv, _ := i.storage.Get(i.idx)
return kv
}
// Attribute is a synonym for Label().
func (i *Iterator) Attribute() KeyValue {
return i.Label()
}
// IndexedLabel returns current index and attribute. Must be called only
// after Next returns true.
func (i *Iterator) IndexedLabel() (int, KeyValue) {
return i.idx, i.Label()
}
// Len returns a number of labels in the iterator's `*Set`.
func (i *Iterator) Len() int {
return i.storage.Len()
}
// ToSlice is a convenience function that creates a slice of labels
// from the passed iterator. The iterator is set up to start from the
// beginning before creating the slice.
func (i *Iterator) ToSlice() []KeyValue {
l := i.Len()
if l == 0 {
return nil
}
i.idx = -1
slice := make([]KeyValue, 0, l)
for i.Next() {
slice = append(slice, i.Label())
}
return slice
}
// NewMergeIterator returns a MergeIterator for merging two label sets
// Duplicates are resolved by taking the value from the first set.
func NewMergeIterator(s1, s2 *Set) MergeIterator {
mi := MergeIterator{
one: makeOne(s1.Iter()),
two: makeOne(s2.Iter()),
}
return mi
}
func makeOne(iter Iterator) oneIterator {
oi := oneIterator{
iter: iter,
}
oi.advance()
return oi
}
func (oi *oneIterator) advance() {
if oi.done = !oi.iter.Next(); !oi.done {
oi.label = oi.iter.Label()
}
}
// Next returns true if there is another label available.
func (m *MergeIterator) Next() bool {
if m.one.done && m.two.done {
return false
}
if m.one.done {
m.current = m.two.label
m.two.advance()
return true
}
if m.two.done {
m.current = m.one.label
m.one.advance()
return true
}
if m.one.label.Key == m.two.label.Key {
m.current = m.one.label // first iterator label value wins
m.one.advance()
m.two.advance()
return true
}
if m.one.label.Key < m.two.label.Key {
m.current = m.one.label
m.one.advance()
return true
}
m.current = m.two.label
m.two.advance()
return true
}
// Label returns the current value after Next() returns true.
func (m *MergeIterator) Label() KeyValue {
return m.current
}

149
attribute/iterator_test.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright The 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 attribute_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
)
func TestIterator(t *testing.T) {
one := attribute.String("one", "1")
two := attribute.Int("two", 2)
lbl := attribute.NewSet(one, two)
iter := lbl.Iter()
require.Equal(t, 2, iter.Len())
require.True(t, iter.Next())
require.Equal(t, one, iter.Label())
idx, attr := iter.IndexedLabel()
require.Equal(t, 0, idx)
require.Equal(t, one, attr)
require.Equal(t, 2, iter.Len())
require.True(t, iter.Next())
require.Equal(t, two, iter.Label())
idx, attr = iter.IndexedLabel()
require.Equal(t, 1, idx)
require.Equal(t, two, attr)
require.Equal(t, 2, iter.Len())
require.False(t, iter.Next())
require.Equal(t, 2, iter.Len())
}
func TestEmptyIterator(t *testing.T) {
lbl := attribute.NewSet()
iter := lbl.Iter()
require.Equal(t, 0, iter.Len())
require.False(t, iter.Next())
}
func TestMergedIterator(t *testing.T) {
type inputs struct {
name string
keys1 []string
keys2 []string
expect []string
}
makeLabels := func(keys []string, num int) (result []attribute.KeyValue) {
for _, k := range keys {
result = append(result, attribute.Int(k, num))
}
return
}
for _, input := range []inputs{
{
name: "one overlap",
keys1: []string{"A", "B"},
keys2: []string{"B", "C"},
expect: []string{"A/1", "B/1", "C/2"},
},
{
name: "reversed one overlap",
keys1: []string{"B", "A"},
keys2: []string{"C", "B"},
expect: []string{"A/1", "B/1", "C/2"},
},
{
name: "one empty",
keys1: nil,
keys2: []string{"C", "B"},
expect: []string{"B/2", "C/2"},
},
{
name: "two empty",
keys1: []string{"C", "B"},
keys2: nil,
expect: []string{"B/1", "C/1"},
},
{
name: "no overlap both",
keys1: []string{"C"},
keys2: []string{"B"},
expect: []string{"B/2", "C/1"},
},
{
name: "one empty single two",
keys1: nil,
keys2: []string{"B"},
expect: []string{"B/2"},
},
{
name: "two empty single one",
keys1: []string{"A"},
keys2: nil,
expect: []string{"A/1"},
},
{
name: "all empty",
keys1: nil,
keys2: nil,
expect: nil,
},
{
name: "full overlap",
keys1: []string{"A", "B", "C", "D"},
keys2: []string{"A", "B", "C", "D"},
expect: []string{"A/1", "B/1", "C/1", "D/1"},
},
} {
t.Run(input.name, func(t *testing.T) {
labels1 := makeLabels(input.keys1, 1)
labels2 := makeLabels(input.keys2, 2)
set1 := attribute.NewSet(labels1...)
set2 := attribute.NewSet(labels2...)
merge := attribute.NewMergeIterator(&set1, &set2)
var result []string
for merge.Next() {
label := merge.Label()
result = append(result, fmt.Sprint(label.Key, "/", label.Value.Emit()))
}
require.Equal(t, input.expect, result)
})
}
}

102
attribute/key.go Normal file
View File

@@ -0,0 +1,102 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
// Key represents the key part in key-value pairs. It's a string. The
// allowed character set in the key depends on the use of the key.
type Key string
// Bool creates a KeyValue instance with a BOOL Value.
//
// If creating both key and a bool value at the same time, then
// instead of calling Key(name).Bool(value) consider using a
// convenience function provided by the api/key package -
// key.Bool(name, value).
func (k Key) Bool(v bool) KeyValue {
return KeyValue{
Key: k,
Value: BoolValue(v),
}
}
// Int64 creates a KeyValue instance with an INT64 Value.
//
// If creating both key and an int64 value at the same time, then
// instead of calling Key(name).Int64(value) consider using a
// convenience function provided by the api/key package -
// key.Int64(name, value).
func (k Key) Int64(v int64) KeyValue {
return KeyValue{
Key: k,
Value: Int64Value(v),
}
}
// Float64 creates a KeyValue instance with a FLOAT64 Value.
//
// If creating both key and a float64 value at the same time, then
// instead of calling Key(name).Float64(value) consider using a
// convenience function provided by the api/key package -
// key.Float64(name, value).
func (k Key) Float64(v float64) KeyValue {
return KeyValue{
Key: k,
Value: Float64Value(v),
}
}
// String creates a KeyValue instance with a STRING Value.
//
// If creating both key and a string value at the same time, then
// instead of calling Key(name).String(value) consider using a
// convenience function provided by the api/key package -
// key.String(name, value).
func (k Key) String(v string) KeyValue {
return KeyValue{
Key: k,
Value: StringValue(v),
}
}
// Int creates a KeyValue instance with an INT64 Value.
//
// If creating both key and an int value at the same time, then
// instead of calling Key(name).Int(value) consider using a
// convenience function provided by the api/key package -
// key.Int(name, value).
func (k Key) Int(v int) KeyValue {
return KeyValue{
Key: k,
Value: IntValue(v),
}
}
// Defined returns true for non-empty keys.
func (k Key) Defined() bool {
return len(k) != 0
}
// Array creates a KeyValue instance with a ARRAY Value.
//
// If creating both key and a array value at the same time, then
// instead of calling Key(name).String(value) consider using a
// convenience function provided by the api/key package -
// key.Array(name, value).
func (k Key) Array(v interface{}) KeyValue {
return KeyValue{
Key: k,
Value: ArrayValue(v),
}
}

101
attribute/key_test.go Normal file
View File

@@ -0,0 +1,101 @@
// Copyright The 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 attribute_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
)
func TestDefined(t *testing.T) {
for _, testcase := range []struct {
name string
k attribute.Key
want bool
}{
{
name: "Key.Defined() returns true when len(v.Name) != 0",
k: attribute.Key("foo"),
want: true,
},
{
name: "Key.Defined() returns false when len(v.Name) == 0",
k: attribute.Key(""),
want: false,
},
} {
t.Run(testcase.name, func(t *testing.T) {
//func (k attribute.Key) Defined() bool {
have := testcase.k.Defined()
if have != testcase.want {
t.Errorf("Want: %v, but have: %v", testcase.want, have)
}
})
}
}
func TestJSONValue(t *testing.T) {
var kvs interface{} = [2]attribute.KeyValue{
attribute.String("A", "B"),
attribute.Int64("C", 1),
}
data, err := json.Marshal(kvs)
require.NoError(t, err)
require.Equal(t,
`[{"Key":"A","Value":{"Type":"STRING","Value":"B"}},{"Key":"C","Value":{"Type":"INT64","Value":1}}]`,
string(data))
}
func TestEmit(t *testing.T) {
for _, testcase := range []struct {
name string
v attribute.Value
want string
}{
{
name: `test Key.Emit() can emit a string representing self.BOOL`,
v: attribute.BoolValue(true),
want: "true",
},
{
name: `test Key.Emit() can emit a string representing self.INT64`,
v: attribute.Int64Value(42),
want: "42",
},
{
name: `test Key.Emit() can emit a string representing self.FLOAT64`,
v: attribute.Float64Value(42.1),
want: "42.1",
},
{
name: `test Key.Emit() can emit a string representing self.STRING`,
v: attribute.StringValue("foo"),
want: "foo",
},
} {
t.Run(testcase.name, func(t *testing.T) {
//proto: func (v attribute.Value) Emit() string {
have := testcase.v.Emit()
if have != testcase.want {
t.Errorf("Want: %s, but have: %s", testcase.want, have)
}
})
}
}

103
attribute/kv.go Normal file
View File

@@ -0,0 +1,103 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/json"
"fmt"
"reflect"
)
// KeyValue holds a key and value pair.
type KeyValue struct {
Key Key
Value Value
}
// Bool creates a new key-value pair with a passed name and a bool
// value.
func Bool(k string, v bool) KeyValue {
return Key(k).Bool(v)
}
// Int64 creates a new key-value pair with a passed name and an int64
// value.
func Int64(k string, v int64) KeyValue {
return Key(k).Int64(v)
}
// Float64 creates a new key-value pair with a passed name and a float64
// value.
func Float64(k string, v float64) KeyValue {
return Key(k).Float64(v)
}
// String creates a new key-value pair with a passed name and a string
// value.
func String(k, v string) KeyValue {
return Key(k).String(v)
}
// Stringer creates a new key-value pair with a passed name and a string
// value generated by the passed Stringer interface.
func Stringer(k string, v fmt.Stringer) KeyValue {
return Key(k).String(v.String())
}
// Int creates a new key-value pair instance with a passed name and
// either an int32 or an int64 value, depending on whether the int
// type is 32 or 64 bits wide.
func Int(k string, v int) KeyValue {
return Key(k).Int(v)
}
// Array creates a new key-value pair with a passed name and a array.
// Only arrays of primitive type are supported.
func Array(k string, v interface{}) KeyValue {
return Key(k).Array(v)
}
// Any creates a new key-value pair instance with a passed name and
// automatic type inference. This is slower, and not type-safe.
func Any(k string, value interface{}) KeyValue {
if value == nil {
return String(k, "<nil>")
}
if stringer, ok := value.(fmt.Stringer); ok {
return String(k, stringer.String())
}
rv := reflect.ValueOf(value)
switch rv.Kind() {
case reflect.Array, reflect.Slice:
return Array(k, value)
case reflect.Bool:
return Bool(k, rv.Bool())
case reflect.Int, reflect.Int8, reflect.Int16:
return Int(k, int(rv.Int()))
case reflect.Int64:
return Int64(k, rv.Int())
case reflect.Float64:
return Float64(k, rv.Float())
case reflect.String:
return String(k, rv.String())
}
if b, err := json.Marshal(value); b != nil && err == nil {
return String(k, string(b))
}
return String(k, fmt.Sprint(value))
}

162
attribute/kv_test.go Normal file
View File

@@ -0,0 +1,162 @@
// Copyright The 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 attribute_test
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"go.opentelemetry.io/otel/attribute"
)
func TestKeyValueConstructors(t *testing.T) {
tt := []struct {
name string
actual attribute.KeyValue
expected attribute.KeyValue
}{
{
name: "Bool",
actual: attribute.Bool("k1", true),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.BoolValue(true),
},
},
{
name: "Int64",
actual: attribute.Int64("k1", 123),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.Int64Value(123),
},
},
{
name: "Float64",
actual: attribute.Float64("k1", 123.5),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.Float64Value(123.5),
},
},
{
name: "String",
actual: attribute.String("k1", "123.5"),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.StringValue("123.5"),
},
},
{
name: "Int",
actual: attribute.Int("k1", 123),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.IntValue(123),
},
},
}
for _, test := range tt {
t.Run(test.name, func(t *testing.T) {
if diff := cmp.Diff(test.actual, test.expected, cmp.AllowUnexported(attribute.Value{})); diff != "" {
t.Fatal(diff)
}
})
}
}
func TestAny(t *testing.T) {
builder := &strings.Builder{}
builder.WriteString("foo")
jsonifyStruct := struct {
Public string
private string
Tagged string `json:"tagName"`
Empty string
OmitEmpty string `json:",omitempty"`
Omit string `json:"-"`
}{"foo", "bar", "baz", "", "", "omitted"}
invalidStruct := struct {
N complex64
}{complex(0, 0)}
for _, testcase := range []struct {
key string
value interface{}
wantType attribute.Type
wantValue interface{}
}{
{
key: "bool type inferred",
value: true,
wantType: attribute.BOOL,
wantValue: true,
},
{
key: "int64 type inferred",
value: int64(42),
wantType: attribute.INT64,
wantValue: int64(42),
},
{
key: "float64 type inferred",
value: float64(42.1),
wantType: attribute.FLOAT64,
wantValue: 42.1,
},
{
key: "string type inferred",
value: "foo",
wantType: attribute.STRING,
wantValue: "foo",
},
{
key: "stringer type inferred",
value: builder,
wantType: attribute.STRING,
wantValue: "foo",
},
{
key: "unknown value serialized as %v",
value: nil,
wantType: attribute.STRING,
wantValue: "<nil>",
},
{
key: "JSON struct serialized correctly",
value: &jsonifyStruct,
wantType: attribute.STRING,
wantValue: `{"Public":"foo","tagName":"baz","Empty":""}`,
},
{
key: "Invalid JSON struct falls back to string",
value: &invalidStruct,
wantType: attribute.STRING,
wantValue: "&{(0+0i)}",
},
} {
t.Logf("Running test case %s", testcase.key)
keyValue := attribute.Any(testcase.key, testcase.value)
if keyValue.Value.Type() != testcase.wantType {
t.Errorf("wrong value type, got %#v, expected %#v", keyValue.Value.Type(), testcase.wantType)
}
got := keyValue.Value.AsInterface()
if diff := cmp.Diff(testcase.wantValue, got); diff != "" {
t.Errorf("+got, -want: %s", diff)
}
}
}

471
attribute/set.go Normal file
View File

@@ -0,0 +1,471 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/json"
"reflect"
"sort"
"sync"
)
type (
// Set is the representation for a distinct label set. It
// manages an immutable set of labels, with an internal cache
// for storing label encodings.
//
// This type supports the `Equivalent` method of comparison
// using values of type `Distinct`.
//
// This type is used to implement:
// 1. Metric labels
// 2. Resource sets
// 3. Correlation map (TODO)
Set struct {
equivalent Distinct
lock sync.Mutex
encoders [maxConcurrentEncoders]EncoderID
encoded [maxConcurrentEncoders]string
}
// Distinct wraps a variable-size array of `KeyValue`,
// constructed with keys in sorted order. This can be used as
// a map key or for equality checking between Sets.
Distinct struct {
iface interface{}
}
// Filter supports removing certain labels from label sets.
// When the filter returns true, the label will be kept in
// the filtered label set. When the filter returns false, the
// label is excluded from the filtered label set, and the
// label instead appears in the `removed` list of excluded labels.
Filter func(KeyValue) bool
// Sortable implements `sort.Interface`, used for sorting
// `KeyValue`. This is an exported type to support a
// memory optimization. A pointer to one of these is needed
// for the call to `sort.Stable()`, which the caller may
// provide in order to avoid an allocation. See
// `NewSetWithSortable()`.
Sortable []KeyValue
)
var (
// keyValueType is used in `computeDistinctReflect`.
keyValueType = reflect.TypeOf(KeyValue{})
// emptySet is returned for empty label sets.
emptySet = &Set{
equivalent: Distinct{
iface: [0]KeyValue{},
},
}
)
const maxConcurrentEncoders = 3
// EmptySet returns a reference to a Set with no elements.
//
// This is a convenience provided for optimized calling utility.
func EmptySet() *Set {
return emptySet
}
// reflect abbreviates `reflect.ValueOf`.
func (d Distinct) reflect() reflect.Value {
return reflect.ValueOf(d.iface)
}
// Valid returns true if this value refers to a valid `*Set`.
func (d Distinct) Valid() bool {
return d.iface != nil
}
// Len returns the number of labels in this set.
func (l *Set) Len() int {
if l == nil || !l.equivalent.Valid() {
return 0
}
return l.equivalent.reflect().Len()
}
// Get returns the KeyValue at ordered position `idx` in this set.
func (l *Set) Get(idx int) (KeyValue, bool) {
if l == nil {
return KeyValue{}, false
}
value := l.equivalent.reflect()
if idx >= 0 && idx < value.Len() {
// Note: The Go compiler successfully avoids an allocation for
// the interface{} conversion here:
return value.Index(idx).Interface().(KeyValue), true
}
return KeyValue{}, false
}
// Value returns the value of a specified key in this set.
func (l *Set) Value(k Key) (Value, bool) {
if l == nil {
return Value{}, false
}
rValue := l.equivalent.reflect()
vlen := rValue.Len()
idx := sort.Search(vlen, func(idx int) bool {
return rValue.Index(idx).Interface().(KeyValue).Key >= k
})
if idx >= vlen {
return Value{}, false
}
keyValue := rValue.Index(idx).Interface().(KeyValue)
if k == keyValue.Key {
return keyValue.Value, true
}
return Value{}, false
}
// HasValue tests whether a key is defined in this set.
func (l *Set) HasValue(k Key) bool {
if l == nil {
return false
}
_, ok := l.Value(k)
return ok
}
// Iter returns an iterator for visiting the labels in this set.
func (l *Set) Iter() Iterator {
return Iterator{
storage: l,
idx: -1,
}
}
// ToSlice returns the set of labels belonging to this set, sorted,
// where keys appear no more than once.
func (l *Set) ToSlice() []KeyValue {
iter := l.Iter()
return iter.ToSlice()
}
// Equivalent returns a value that may be used as a map key. The
// Distinct type guarantees that the result will equal the equivalent
// Distinct value of any label set with the same elements as this,
// where sets are made unique by choosing the last value in the input
// for any given key.
func (l *Set) Equivalent() Distinct {
if l == nil || !l.equivalent.Valid() {
return emptySet.equivalent
}
return l.equivalent
}
// Equals returns true if the argument set is equivalent to this set.
func (l *Set) Equals(o *Set) bool {
return l.Equivalent() == o.Equivalent()
}
// Encoded returns the encoded form of this set, according to
// `encoder`. The result will be cached in this `*Set`.
func (l *Set) Encoded(encoder Encoder) string {
if l == nil || encoder == nil {
return ""
}
id := encoder.ID()
if !id.Valid() {
// Invalid IDs are not cached.
return encoder.Encode(l.Iter())
}
var lookup *string
l.lock.Lock()
for idx := 0; idx < maxConcurrentEncoders; idx++ {
if l.encoders[idx] == id {
lookup = &l.encoded[idx]
break
}
}
l.lock.Unlock()
if lookup != nil {
return *lookup
}
r := encoder.Encode(l.Iter())
l.lock.Lock()
defer l.lock.Unlock()
for idx := 0; idx < maxConcurrentEncoders; idx++ {
if l.encoders[idx] == id {
return l.encoded[idx]
}
if !l.encoders[idx].Valid() {
l.encoders[idx] = id
l.encoded[idx] = r
return r
}
}
// TODO: This is a performance cliff. Find a way for this to
// generate a warning.
return r
}
func empty() Set {
return Set{
equivalent: emptySet.equivalent,
}
}
// NewSet returns a new `Set`. See the documentation for
// `NewSetWithSortableFiltered` for more details.
//
// Except for empty sets, this method adds an additional allocation
// compared with calls that include a `*Sortable`.
func NewSet(kvs ...KeyValue) Set {
// Check for empty set.
if len(kvs) == 0 {
return empty()
}
s, _ := NewSetWithSortableFiltered(kvs, new(Sortable), nil)
return s //nolint
}
// NewSetWithSortable returns a new `Set`. See the documentation for
// `NewSetWithSortableFiltered` for more details.
//
// This call includes a `*Sortable` option as a memory optimization.
func NewSetWithSortable(kvs []KeyValue, tmp *Sortable) Set {
// Check for empty set.
if len(kvs) == 0 {
return empty()
}
s, _ := NewSetWithSortableFiltered(kvs, tmp, nil)
return s //nolint
}
// NewSetWithFiltered returns a new `Set`. See the documentation for
// `NewSetWithSortableFiltered` for more details.
//
// This call includes a `Filter` to include/exclude label keys from
// the return value. Excluded keys are returned as a slice of label
// values.
func NewSetWithFiltered(kvs []KeyValue, filter Filter) (Set, []KeyValue) {
// Check for empty set.
if len(kvs) == 0 {
return empty(), nil
}
return NewSetWithSortableFiltered(kvs, new(Sortable), filter)
}
// NewSetWithSortableFiltered returns a new `Set`.
//
// Duplicate keys are eliminated by taking the last value. This
// re-orders the input slice so that unique last-values are contiguous
// at the end of the slice.
//
// This ensures the following:
//
// - Last-value-wins semantics
// - Caller sees the reordering, but doesn't lose values
// - Repeated call preserve last-value wins.
//
// Note that methods are defined on `*Set`, although this returns `Set`.
// Callers can avoid memory allocations by:
//
// - allocating a `Sortable` for use as a temporary in this method
// - allocating a `Set` for storing the return value of this
// constructor.
//
// The result maintains a cache of encoded labels, by attribute.EncoderID.
// This value should not be copied after its first use.
//
// The second `[]KeyValue` return value is a list of labels that were
// excluded by the Filter (if non-nil).
func NewSetWithSortableFiltered(kvs []KeyValue, tmp *Sortable, filter Filter) (Set, []KeyValue) {
// Check for empty set.
if len(kvs) == 0 {
return empty(), nil
}
*tmp = kvs
// Stable sort so the following de-duplication can implement
// last-value-wins semantics.
sort.Stable(tmp)
*tmp = nil
position := len(kvs) - 1
offset := position - 1
// The requirements stated above require that the stable
// result be placed in the end of the input slice, while
// overwritten values are swapped to the beginning.
//
// De-duplicate with last-value-wins semantics. Preserve
// duplicate values at the beginning of the input slice.
for ; offset >= 0; offset-- {
if kvs[offset].Key == kvs[position].Key {
continue
}
position--
kvs[offset], kvs[position] = kvs[position], kvs[offset]
}
if filter != nil {
return filterSet(kvs[position:], filter)
}
return Set{
equivalent: computeDistinct(kvs[position:]),
}, nil
}
// filterSet reorders `kvs` so that included keys are contiguous at
// the end of the slice, while excluded keys precede the included keys.
func filterSet(kvs []KeyValue, filter Filter) (Set, []KeyValue) {
var excluded []KeyValue
// Move labels that do not match the filter so
// they're adjacent before calling computeDistinct().
distinctPosition := len(kvs)
// Swap indistinct keys forward and distinct keys toward the
// end of the slice.
offset := len(kvs) - 1
for ; offset >= 0; offset-- {
if filter(kvs[offset]) {
distinctPosition--
kvs[offset], kvs[distinctPosition] = kvs[distinctPosition], kvs[offset]
continue
}
}
excluded = kvs[:distinctPosition]
return Set{
equivalent: computeDistinct(kvs[distinctPosition:]),
}, excluded
}
// Filter returns a filtered copy of this `Set`. See the
// documentation for `NewSetWithSortableFiltered` for more details.
func (l *Set) Filter(re Filter) (Set, []KeyValue) {
if re == nil {
return Set{
equivalent: l.equivalent,
}, nil
}
// Note: This could be refactored to avoid the temporary slice
// allocation, if it proves to be expensive.
return filterSet(l.ToSlice(), re)
}
// computeDistinct returns a `Distinct` using either the fixed- or
// reflect-oriented code path, depending on the size of the input.
// The input slice is assumed to already be sorted and de-duplicated.
func computeDistinct(kvs []KeyValue) Distinct {
iface := computeDistinctFixed(kvs)
if iface == nil {
iface = computeDistinctReflect(kvs)
}
return Distinct{
iface: iface,
}
}
// computeDistinctFixed computes a `Distinct` for small slices. It
// returns nil if the input is too large for this code path.
func computeDistinctFixed(kvs []KeyValue) interface{} {
switch len(kvs) {
case 1:
ptr := new([1]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 2:
ptr := new([2]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 3:
ptr := new([3]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 4:
ptr := new([4]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 5:
ptr := new([5]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 6:
ptr := new([6]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 7:
ptr := new([7]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 8:
ptr := new([8]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 9:
ptr := new([9]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
case 10:
ptr := new([10]KeyValue)
copy((*ptr)[:], kvs)
return *ptr
default:
return nil
}
}
// computeDistinctReflect computes a `Distinct` using reflection,
// works for any size input.
func computeDistinctReflect(kvs []KeyValue) interface{} {
at := reflect.New(reflect.ArrayOf(len(kvs), keyValueType)).Elem()
for i, keyValue := range kvs {
*(at.Index(i).Addr().Interface().(*KeyValue)) = keyValue
}
return at.Interface()
}
// MarshalJSON returns the JSON encoding of the `*Set`.
func (l *Set) MarshalJSON() ([]byte, error) {
return json.Marshal(l.equivalent.iface)
}
// Len implements `sort.Interface`.
func (l *Sortable) Len() int {
return len(*l)
}
// Swap implements `sort.Interface`.
func (l *Sortable) Swap(i, j int) {
(*l)[i], (*l)[j] = (*l)[j], (*l)[i]
}
// Less implements `sort.Interface`.
func (l *Sortable) Less(i, j int) bool {
return (*l)[i].Key < (*l)[j].Key
}

190
attribute/set_test.go Normal file
View File

@@ -0,0 +1,190 @@
// Copyright The 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 attribute_test
import (
"regexp"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
)
type testCase struct {
kvs []attribute.KeyValue
keyRe *regexp.Regexp
encoding string
fullEnc string
}
func expect(enc string, kvs ...attribute.KeyValue) testCase {
return testCase{
kvs: kvs,
encoding: enc,
}
}
func expectFiltered(enc, filter, fullEnc string, kvs ...attribute.KeyValue) testCase {
return testCase{
kvs: kvs,
keyRe: regexp.MustCompile(filter),
encoding: enc,
fullEnc: fullEnc,
}
}
func TestSetDedup(t *testing.T) {
cases := []testCase{
expect("A=B", attribute.String("A", "2"), attribute.String("A", "B")),
expect("A=B", attribute.String("A", "2"), attribute.Int("A", 1), attribute.String("A", "B")),
expect("A=B", attribute.String("A", "B"), attribute.String("A", "C"), attribute.String("A", "D"), attribute.String("A", "B")),
expect("A=B,C=D", attribute.String("A", "1"), attribute.String("C", "D"), attribute.String("A", "B")),
expect("A=B,C=D", attribute.String("A", "2"), attribute.String("A", "B"), attribute.String("C", "D")),
expect("A=B,C=D", attribute.Float64("C", 1.2), attribute.String("A", "2"), attribute.String("A", "B"), attribute.String("C", "D")),
expect("A=B,C=D", attribute.String("C", "D"), attribute.String("A", "B"), attribute.String("A", "C"), attribute.String("A", "D"), attribute.String("A", "B")),
expect("A=B,C=D", attribute.String("A", "B"), attribute.String("C", "D"), attribute.String("A", "C"), attribute.String("A", "D"), attribute.String("A", "B")),
expect("A=B,C=D", attribute.String("A", "B"), attribute.String("A", "C"), attribute.String("A", "D"), attribute.String("A", "B"), attribute.String("C", "D")),
}
enc := attribute.DefaultEncoder()
s2d := map[string][]attribute.Distinct{}
d2s := map[attribute.Distinct][]string{}
for _, tc := range cases {
cpy := make([]attribute.KeyValue, len(tc.kvs))
copy(cpy, tc.kvs)
sl := attribute.NewSet(cpy...)
// Ensure that the input was reordered but no elements went missing.
require.ElementsMatch(t, tc.kvs, cpy)
str := sl.Encoded(enc)
equ := sl.Equivalent()
s2d[str] = append(s2d[str], equ)
d2s[equ] = append(d2s[equ], str)
require.Equal(t, tc.encoding, str)
}
for s, d := range s2d {
// No other Distinct values are equal to this.
for s2, d2 := range s2d {
if s2 == s {
continue
}
for _, elt := range d {
for _, otherDistinct := range d2 {
require.NotEqual(t, otherDistinct, elt)
}
}
}
for _, strings := range d2s {
if strings[0] == s {
continue
}
for _, otherString := range strings {
require.NotEqual(t, otherString, s)
}
}
}
for d, s := range d2s {
// No other Distinct values are equal to this.
for d2, s2 := range d2s {
if d2 == d {
continue
}
for _, elt := range s {
for _, otherDistinct := range s2 {
require.NotEqual(t, otherDistinct, elt)
}
}
}
for _, distincts := range s2d {
if distincts[0] == d {
continue
}
for _, otherDistinct := range distincts {
require.NotEqual(t, otherDistinct, d)
}
}
}
}
func TestUniqueness(t *testing.T) {
short := []attribute.KeyValue{
attribute.String("A", "0"),
attribute.String("B", "2"),
attribute.String("A", "1"),
}
long := []attribute.KeyValue{
attribute.String("B", "2"),
attribute.String("C", "5"),
attribute.String("B", "2"),
attribute.String("C", "1"),
attribute.String("A", "4"),
attribute.String("C", "3"),
attribute.String("A", "1"),
}
cases := []testCase{
expectFiltered("A=1", "^A$", "B=2", short...),
expectFiltered("B=2", "^B$", "A=1", short...),
expectFiltered("A=1,B=2", "^A|B$", "", short...),
expectFiltered("", "^C", "A=1,B=2", short...),
expectFiltered("A=1,C=3", "A|C", "B=2", long...),
expectFiltered("B=2,C=3", "C|B", "A=1", long...),
expectFiltered("C=3", "C", "A=1,B=2", long...),
expectFiltered("", "D", "A=1,B=2,C=3", long...),
}
enc := attribute.DefaultEncoder()
for _, tc := range cases {
cpy := make([]attribute.KeyValue, len(tc.kvs))
copy(cpy, tc.kvs)
distinct, uniq := attribute.NewSetWithFiltered(cpy, func(label attribute.KeyValue) bool {
return tc.keyRe.MatchString(string(label.Key))
})
full := attribute.NewSet(uniq...)
require.Equal(t, tc.encoding, distinct.Encoded(enc))
require.Equal(t, tc.fullEnc, full.Encoded(enc))
}
}
func TestLookup(t *testing.T) {
set := attribute.NewSet(attribute.Int("C", 3), attribute.Int("A", 1), attribute.Int("B", 2))
value, has := set.Value("C")
require.True(t, has)
require.Equal(t, int64(3), value.AsInt64())
value, has = set.Value("B")
require.True(t, has)
require.Equal(t, int64(2), value.AsInt64())
value, has = set.Value("A")
require.True(t, has)
require.Equal(t, int64(1), value.AsInt64())
value, has = set.Value("D")
require.False(t, has)
}

28
attribute/type_string.go Normal file
View File

@@ -0,0 +1,28 @@
// Code generated by "stringer -type=Type"; DO NOT EDIT.
package attribute
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[INVALID-0]
_ = x[BOOL-1]
_ = x[INT64-2]
_ = x[FLOAT64-3]
_ = x[STRING-4]
_ = x[ARRAY-5]
}
const _Type_name = "INVALIDBOOLINT64FLOAT64STRINGARRAY"
var _Type_index = [...]uint8{0, 7, 11, 16, 23, 29, 34}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}

204
attribute/value.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright The 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 attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"go.opentelemetry.io/otel/internal"
)
//go:generate stringer -type=Type
// Type describes the type of the data Value holds.
type Type int
// Value represents the value part in key-value pairs.
type Value struct {
vtype Type
numeric uint64
stringly string
// TODO Lazy value type?
array interface{}
}
const (
// INVALID is used for a Value with no value set.
INVALID Type = iota
// BOOL is a boolean Type Value.
BOOL
// INT64 is a 64-bit signed integral Type Value.
INT64
// UINT32 is a 32-bit unsigned integral Type Value.
FLOAT64
// STRING is a string Type Value.
STRING
// ARRAY is an array Type Value used to store 1-dimensional slices or
// arrays of bool, int, int32, int64, uint, uint32, uint64, float,
// float32, float64, or string types.
ARRAY
)
// BoolValue creates a BOOL Value.
func BoolValue(v bool) Value {
return Value{
vtype: BOOL,
numeric: internal.BoolToRaw(v),
}
}
// Int64Value creates an INT64 Value.
func Int64Value(v int64) Value {
return Value{
vtype: INT64,
numeric: internal.Int64ToRaw(v),
}
}
// Float64Value creates a FLOAT64 Value.
func Float64Value(v float64) Value {
return Value{
vtype: FLOAT64,
numeric: internal.Float64ToRaw(v),
}
}
// StringValue creates a STRING Value.
func StringValue(v string) Value {
return Value{
vtype: STRING,
stringly: v,
}
}
// IntValue creates an INT64 Value.
func IntValue(v int) Value {
return Int64Value(int64(v))
}
// ArrayValue creates an ARRAY value from an array or slice.
// Only arrays or slices of bool, int, int64, float, float64, or string types are allowed.
// Specifically, arrays and slices can not contain other arrays, slices, structs, or non-standard
// types. If the passed value is not an array or slice of these types an
// INVALID value is returned.
func ArrayValue(v interface{}) Value {
switch reflect.TypeOf(v).Kind() {
case reflect.Array, reflect.Slice:
// get array type regardless of dimensions
typ := reflect.TypeOf(v).Elem()
kind := typ.Kind()
switch kind {
case reflect.Bool, reflect.Int, reflect.Int64,
reflect.Float64, reflect.String:
val := reflect.ValueOf(v)
length := val.Len()
frozen := reflect.Indirect(reflect.New(reflect.ArrayOf(length, typ)))
reflect.Copy(frozen, val)
return Value{
vtype: ARRAY,
array: frozen.Interface(),
}
default:
return Value{vtype: INVALID}
}
}
return Value{vtype: INVALID}
}
// Type returns a type of the Value.
func (v Value) Type() Type {
return v.vtype
}
// AsBool returns the bool value. Make sure that the Value's type is
// BOOL.
func (v Value) AsBool() bool {
return internal.RawToBool(v.numeric)
}
// AsInt64 returns the int64 value. Make sure that the Value's type is
// INT64.
func (v Value) AsInt64() int64 {
return internal.RawToInt64(v.numeric)
}
// AsFloat64 returns the float64 value. Make sure that the Value's
// type is FLOAT64.
func (v Value) AsFloat64() float64 {
return internal.RawToFloat64(v.numeric)
}
// AsString returns the string value. Make sure that the Value's type
// is STRING.
func (v Value) AsString() string {
return v.stringly
}
// AsArray returns the array Value as an interface{}.
func (v Value) AsArray() interface{} {
return v.array
}
type unknownValueType struct{}
// AsInterface returns Value's data as interface{}.
func (v Value) AsInterface() interface{} {
switch v.Type() {
case ARRAY:
return v.AsArray()
case BOOL:
return v.AsBool()
case INT64:
return v.AsInt64()
case FLOAT64:
return v.AsFloat64()
case STRING:
return v.stringly
}
return unknownValueType{}
}
// Emit returns a string representation of Value's data.
func (v Value) Emit() string {
switch v.Type() {
case ARRAY:
return fmt.Sprint(v.array)
case BOOL:
return strconv.FormatBool(v.AsBool())
case INT64:
return strconv.FormatInt(v.AsInt64(), 10)
case FLOAT64:
return fmt.Sprint(v.AsFloat64())
case STRING:
return v.stringly
default:
return "unknown"
}
}
// MarshalJSON returns the JSON encoding of the Value.
func (v Value) MarshalJSON() ([]byte, error) {
var jsonVal struct {
Type string
Value interface{}
}
jsonVal.Type = v.Type().String()
jsonVal.Value = v.AsInterface()
return json.Marshal(jsonVal)
}

138
attribute/value_test.go Normal file
View File

@@ -0,0 +1,138 @@
// Copyright The 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 attribute_test
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"go.opentelemetry.io/otel/attribute"
)
func TestValue(t *testing.T) {
k := attribute.Key("test")
bli := getBitlessInfo(42)
for _, testcase := range []struct {
name string
value attribute.Value
wantType attribute.Type
wantValue interface{}
}{
{
name: "Key.Bool() correctly returns keys's internal bool value",
value: k.Bool(true).Value,
wantType: attribute.BOOL,
wantValue: true,
},
{
name: "Key.Array([]bool) correctly return key's internal bool values",
value: k.Array([]bool{true, false}).Value,
wantType: attribute.ARRAY,
wantValue: [2]bool{true, false},
},
{
name: "Key.Int64() correctly returns keys's internal int64 value",
value: k.Int64(42).Value,
wantType: attribute.INT64,
wantValue: int64(42),
},
{
name: "Key.Float64() correctly returns keys's internal float64 value",
value: k.Float64(42.1).Value,
wantType: attribute.FLOAT64,
wantValue: 42.1,
},
{
name: "Key.String() correctly returns keys's internal string value",
value: k.String("foo").Value,
wantType: attribute.STRING,
wantValue: "foo",
},
{
name: "Key.Int() correctly returns keys's internal signed integral value",
value: k.Int(bli.intValue).Value,
wantType: bli.signedType,
wantValue: bli.signedValue,
},
{
name: "Key.Array([]int64) correctly returns keys's internal int64 values",
value: k.Array([]int64{42, 43}).Value,
wantType: attribute.ARRAY,
wantValue: [2]int64{42, 43},
},
{
name: "Key.Array([]float64) correctly returns keys's internal float64 values",
value: k.Array([]float64{42, 43}).Value,
wantType: attribute.ARRAY,
wantValue: [2]float64{42, 43},
},
{
name: "Key.Array([]string) correctly return key's internal string values",
value: k.Array([]string{"foo", "bar"}).Value,
wantType: attribute.ARRAY,
wantValue: [2]string{"foo", "bar"},
},
{
name: "Key.Array([]int) correctly returns keys's internal signed integral values",
value: k.Array([]int{42, 43}).Value,
wantType: attribute.ARRAY,
wantValue: [2]int{42, 43},
},
{
name: "Key.Array([][]int) refuses to create multi-dimensional array",
value: k.Array([][]int{{1, 2}, {3, 4}}).Value,
wantType: attribute.INVALID,
wantValue: nil,
},
} {
t.Logf("Running test case %s", testcase.name)
if testcase.value.Type() != testcase.wantType {
t.Errorf("wrong value type, got %#v, expected %#v", testcase.value.Type(), testcase.wantType)
}
if testcase.wantType == attribute.INVALID {
continue
}
got := testcase.value.AsInterface()
if diff := cmp.Diff(testcase.wantValue, got); diff != "" {
t.Errorf("+got, -want: %s", diff)
}
}
}
type bitlessInfo struct {
intValue int
uintValue uint
signedType attribute.Type
signedValue interface{}
}
func getBitlessInfo(i int) bitlessInfo {
return bitlessInfo{
intValue: i,
uintValue: uint(i),
signedType: attribute.INT64,
signedValue: int64(i),
}
}
func TestAsArrayValue(t *testing.T) {
v := attribute.ArrayValue([]int{1, 2, 3}).AsArray()
// Ensure the returned dynamic type is stable.
if got, want := reflect.TypeOf(v).Kind(), reflect.Array; got != want {
t.Errorf("AsArray() returned %T, want %T", got, want)
}
}