Skip to content

Commit

Permalink
New api/label package, common label set impl (#651)
Browse files Browse the repository at this point in the history
* New label set API

* Checkpoint

* Remove label.Labels interface

* Fix trace

* Remove label storage

* Restore metric_test.go

* Tidy tests

* More comments

* More comments

* Same changes as 654

* Checkpoint

* Fix batch labels

* Avoid Resource.Attributes() where possible

* Update comments and restore order in resource.go

* From feedback

* From feedback

* Move iterator_test & feedback

* Strenghten the label.Set test

* Feedback on typos

* Fix the set test per @krnowak

* Nit
  • Loading branch information
jmacd authored Apr 23, 2020
1 parent acb350b commit 0bb12d9
Show file tree
Hide file tree
Showing 44 changed files with 1,156 additions and 939 deletions.
152 changes: 152 additions & 0 deletions api/label/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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 label

import (
"bytes"
"sync"
"sync/atomic"

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

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 label.Set.
Encode(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, kv := iter.IndexedLabel()
if i > 0 {
_, _ = buf.WriteRune(',')
}
copyAndEscape(buf, string(kv.Key))

_, _ = buf.WriteRune('=')

if kv.Value.Type() == core.STRING {
copyAndEscape(buf, kv.Value.AsString())
} else {
_, _ = buf.WriteString(kv.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
}
77 changes: 77 additions & 0 deletions api/label/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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 label

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

// Iterator allows iterating over the set of labels in order,
// sorted by key.
type Iterator struct {
storage *Set
idx int
}

// 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 core.KeyValue. Must be called only after Next returns
// true.
func (i *Iterator) Label() core.KeyValue {
kv, _ := i.storage.Get(i.idx)
return kv
}

// Attribute is a synonym for Label().
func (i *Iterator) Attribute() core.KeyValue {
return i.Label()
}

// IndexedLabel returns current index and label. Must be called only
// after Next returns true.
func (i *Iterator) IndexedLabel() (int, core.KeyValue) {
return i.idx, i.Label()
}

// IndexedAttribute is a synonym for IndexedLabel().
func (i *Iterator) IndexedAttribute() (int, core.KeyValue) {
return i.IndexedLabel()
}

// 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() []core.KeyValue {
l := i.Len()
if l == 0 {
return nil
}
i.idx = -1
slice := make([]core.KeyValue, 0, l)
for i.Next() {
slice = append(slice, i.Label())
}
return slice
}
22 changes: 12 additions & 10 deletions sdk/resource/iterator_test.go → api/label/iterator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package resource
package label_test

import (
"testing"

"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/api/core"
"go.opentelemetry.io/otel/api/key"
"go.opentelemetry.io/otel/api/label"
)

func TestAttributeIterator(t *testing.T) {
func TestIterator(t *testing.T) {
one := key.String("one", "1")
two := key.Int("two", 2)
iter := NewAttributeIterator([]core.KeyValue{one, two})
lbl := label.NewSet(one, two)
iter := lbl.Iter()
require.Equal(t, 2, iter.Len())

require.True(t, iter.Next())
require.Equal(t, one, iter.Attribute())
idx, attr := iter.IndexedAttribute()
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.Attribute())
idx, attr = iter.IndexedAttribute()
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())
Expand All @@ -47,8 +48,9 @@ func TestAttributeIterator(t *testing.T) {
require.Equal(t, 2, iter.Len())
}

func TestEmptyAttributeIterator(t *testing.T) {
iter := NewAttributeIterator(nil)
func TestEmptyIterator(t *testing.T) {
lbl := label.NewSet()
iter := lbl.Iter()
require.Equal(t, 0, iter.Len())
require.False(t, iter.Next())
}
Loading

0 comments on commit 0bb12d9

Please sign in to comment.