mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-12 10:04:29 +02:00
Replace Ordered
with an iterator in export.Labels
. (#567)
* Do not expose a slice of labels in export.Record This is really an inconvenient implementation detail leak - we may want to store labels in a different way. Replace it with an iterator - it does not force us to use slice of key values as a storage in the long run. * Add Len to LabelIterator It may come in handy in several situations, where we don't have access to export.Labels object, but only to the label iterator. * Use reflect value label iterator for the fixed labels * add reset operation to iterator Makes my life easier when writing a benchmark. Might also be an alternative to cloning the iterator. * Add benchmarks for iterators * Add import comment * Add clone operation to label iterator * Move iterator tests to a separate package * Add tests for cloning iterators * Pass label iterator to export labels * Use non-addressable array reflect values By not using the value created by `reflect.New()`, but rather by `reflect.ValueOf()`, we get a non-addressable array in the value, which does not infer an allocation cost when getting an element from the array. * Drop zero iterator This can be substituted by a reflect value iterator that goes over a value with a zero-sized array. * Add a simple iterator that implements label iterator In the long run this will completely replace the LabelIterator interface. * Replace reflect value iterator with simple iterator * Pass label storage to new export labels, not label iterator * Drop label iterator interface, rename storage iterator to label iterator * Drop clone operation from iterator It's a leftover from interface times and now it's pointless - the iterator is a simple struct, so cloning it is a simple copy. * Drop Reset from label iterator The sole existence of Reset was actually for benchmarking convenience. Now we can just copy the iterator cheaply, so a need for Reset is no more. * Drop noop iterator tests * Move back iterator tests to export package * Eagerly get the reflect value of ordered labels So we won't get into problems when several goroutines want to iterate the same labels at the same time. Not sure if this would be a big deal, since every goroutine would compute the same reflect.Value, but concurrent write to the same memory is bad anyway. And it doesn't cost us any extra allocations anyway. * Replace NewSliceLabelIterator() with a method of LabelSlice * Add some documentation * Documentation fixes
This commit is contained in:
parent
d8682c1999
commit
a01f63bec4
@ -62,7 +62,9 @@ type noTagsAdapter struct {
|
||||
func (*noTagsAdapter) AppendName(rec export.Record, buf *bytes.Buffer) {
|
||||
_, _ = buf.WriteString(rec.Descriptor().Name())
|
||||
|
||||
for _, tag := range rec.Labels().Ordered() {
|
||||
iter := rec.Labels().Iter()
|
||||
for iter.Next() {
|
||||
tag := iter.Label()
|
||||
_, _ = buf.WriteString(".")
|
||||
_, _ = buf.WriteString(tag.Value.Emit())
|
||||
}
|
||||
@ -294,7 +296,9 @@ func TestPacketSplit(t *testing.T) {
|
||||
tcase.setup(func(nkeys int) {
|
||||
labels := makeLabels(offset, nkeys)
|
||||
offset += nkeys
|
||||
expect := fmt.Sprint("counter:100|c", adapter.LabelEncoder.Encode(labels), "\n")
|
||||
iter := export.LabelSlice(labels).Iter()
|
||||
encoded := adapter.LabelEncoder.Encode(iter)
|
||||
expect := fmt.Sprint("counter:100|c", encoded, "\n")
|
||||
expected = append(expected, expect)
|
||||
checkpointSet.AddCounter(&desc, 100, labels...)
|
||||
})
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"bytes"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
)
|
||||
|
||||
@ -52,14 +51,15 @@ func NewLabelEncoder() *LabelEncoder {
|
||||
}
|
||||
|
||||
// Encode emits a string like "|#key1:value1,key2:value2".
|
||||
func (e *LabelEncoder) Encode(labels []core.KeyValue) string {
|
||||
func (e *LabelEncoder) Encode(iter export.LabelIterator) string {
|
||||
buf := e.pool.Get().(*bytes.Buffer)
|
||||
defer e.pool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
delimiter := "|#"
|
||||
|
||||
for _, kv := range labels {
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
_, _ = buf.WriteString(delimiter)
|
||||
_, _ = buf.WriteString(string(kv.Key))
|
||||
_, _ = buf.WriteRune(':')
|
||||
@ -80,5 +80,5 @@ func (e *LabelEncoder) ForceEncode(labels export.Labels) (string, bool) {
|
||||
return labels.Encoded(), false
|
||||
}
|
||||
|
||||
return e.Encode(labels.Ordered()), true
|
||||
return e.Encode(labels.Iter()), true
|
||||
}
|
||||
|
@ -35,21 +35,23 @@ var testLabels = []core.KeyValue{
|
||||
func TestLabelSyntax(t *testing.T) {
|
||||
encoder := statsd.NewLabelEncoder()
|
||||
|
||||
require.Equal(t, `|#A:B,C:D,E:1.5`, encoder.Encode(testLabels))
|
||||
require.Equal(t, `|#A:B,C:D,E:1.5`, encoder.Encode(export.LabelSlice(testLabels).Iter()))
|
||||
|
||||
require.Equal(t, `|#A:B`, encoder.Encode([]core.KeyValue{
|
||||
kvs := []core.KeyValue{
|
||||
key.New("A").String("B"),
|
||||
}))
|
||||
}
|
||||
require.Equal(t, `|#A:B`, encoder.Encode(export.LabelSlice(kvs).Iter()))
|
||||
|
||||
require.Equal(t, "", encoder.Encode(nil))
|
||||
require.Equal(t, "", encoder.Encode(export.LabelSlice(nil).Iter()))
|
||||
}
|
||||
|
||||
func TestLabelForceEncode(t *testing.T) {
|
||||
defaultLabelEncoder := sdk.NewDefaultLabelEncoder()
|
||||
statsdLabelEncoder := statsd.NewLabelEncoder()
|
||||
|
||||
exportLabelsDefault := export.NewLabels(testLabels, defaultLabelEncoder.Encode(testLabels), defaultLabelEncoder)
|
||||
exportLabelsStatsd := export.NewLabels(testLabels, statsdLabelEncoder.Encode(testLabels), statsdLabelEncoder)
|
||||
ls := export.LabelSlice(testLabels)
|
||||
exportLabelsDefault := export.NewLabels(ls, defaultLabelEncoder.Encode(ls.Iter()), defaultLabelEncoder)
|
||||
exportLabelsStatsd := export.NewLabels(ls, statsdLabelEncoder.Encode(ls.Iter()), statsdLabelEncoder)
|
||||
|
||||
statsdEncoding := exportLabelsStatsd.Encoded()
|
||||
require.NotEqual(t, statsdEncoding, exportLabelsDefault.Encoded())
|
||||
@ -63,7 +65,7 @@ func TestLabelForceEncode(t *testing.T) {
|
||||
require.False(t, repeat)
|
||||
|
||||
// Check that this works for an embedded implementation.
|
||||
exportLabelsEmbed := export.NewLabels(testLabels, statsdEncoding, struct {
|
||||
exportLabelsEmbed := export.NewLabels(export.LabelSlice(testLabels), statsdEncoding, struct {
|
||||
*statsd.LabelEncoder
|
||||
}{LabelEncoder: statsdLabelEncoder})
|
||||
|
||||
|
@ -299,8 +299,10 @@ func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func labelsKeys(labels export.Labels) []string {
|
||||
keys := make([]string, 0, labels.Len())
|
||||
for _, kv := range labels.Ordered() {
|
||||
iter := labels.Iter()
|
||||
keys := make([]string, 0, iter.Len())
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
keys = append(keys, sanitize(string(kv.Key)))
|
||||
}
|
||||
return keys
|
||||
@ -309,8 +311,10 @@ func labelsKeys(labels export.Labels) []string {
|
||||
func labelValues(labels export.Labels) []string {
|
||||
// TODO(paivagustavo): parse the labels.Encoded() instead of calling `Emit()` directly
|
||||
// this would avoid unnecessary allocations.
|
||||
values := make([]string, 0, labels.Len())
|
||||
for _, label := range labels.Ordered() {
|
||||
iter := labels.Iter()
|
||||
values := make([]string, 0, iter.Len())
|
||||
for iter.Next() {
|
||||
label := iter.Label()
|
||||
values = append(values, label.Value.Emit())
|
||||
}
|
||||
return values
|
||||
|
@ -209,14 +209,16 @@ func (e *Exporter) Export(_ context.Context, checkpointSet export.CheckpointSet)
|
||||
}
|
||||
|
||||
specifiedKeyMap := make(map[core.Key]core.Value)
|
||||
for _, kv := range record.Labels().Ordered() {
|
||||
iter := record.Labels().Iter()
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
specifiedKeyMap[kv.Key] = kv.Value
|
||||
}
|
||||
|
||||
var materializedKeys []string
|
||||
|
||||
if labels := record.Labels(); labels.Len() > 0 {
|
||||
materializedKeys = append(materializedKeys, labels.Encoded())
|
||||
if iter.Len() > 0 {
|
||||
materializedKeys = append(materializedKeys, record.Labels().Encoded())
|
||||
}
|
||||
|
||||
for _, k := range desc.Keys() {
|
||||
|
@ -38,8 +38,8 @@ func (p *CheckpointSet) Reset() {
|
||||
// If there is an existing record with the same descriptor and LabelSet
|
||||
// the stored aggregator will be returned and should be merged.
|
||||
func (p *CheckpointSet) Add(desc *metric.Descriptor, newAgg export.Aggregator, labels ...core.KeyValue) (agg export.Aggregator, added bool) {
|
||||
encoded := p.encoder.Encode(labels)
|
||||
elabels := export.NewLabels(labels, encoded, p.encoder)
|
||||
ls := export.LabelSlice(labels)
|
||||
elabels := export.NewLabels(ls, p.encoder.Encode(ls.Iter()), p.encoder)
|
||||
|
||||
key := desc.Name() + "_" + elabels.Encoded()
|
||||
if record, ok := p.records[key]; ok {
|
||||
|
@ -58,7 +58,7 @@ func sum(desc *metric.Descriptor, labels export.Labels, a aggregator.Sum) (*metr
|
||||
Name: desc.Name(),
|
||||
Description: desc.Description(),
|
||||
Unit: string(desc.Unit()),
|
||||
Labels: stringKeyValues(labels.Ordered()),
|
||||
Labels: stringKeyValues(labels.Iter()),
|
||||
},
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ func minMaxSumCount(desc *metric.Descriptor, labels export.Labels, a aggregator.
|
||||
Description: desc.Description(),
|
||||
Unit: string(desc.Unit()),
|
||||
Type: metricpb.MetricDescriptor_SUMMARY,
|
||||
Labels: stringKeyValues(labels.Ordered()),
|
||||
Labels: stringKeyValues(labels.Iter()),
|
||||
},
|
||||
SummaryDataPoints: []*metricpb.SummaryDataPoint{
|
||||
{
|
||||
@ -131,10 +131,18 @@ func minMaxSumCount(desc *metric.Descriptor, labels export.Labels, a aggregator.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// stringKeyValues transforms a KeyValues into an OTLP StringKeyValues.
|
||||
func stringKeyValues(kvs []core.KeyValue) []*commonpb.StringKeyValue {
|
||||
result := make([]*commonpb.StringKeyValue, 0, len(kvs))
|
||||
for _, kv := range kvs {
|
||||
// stringKeyValues transforms a label iterator into an OTLP StringKeyValues.
|
||||
func stringKeyValues(iter export.LabelIterator) []*commonpb.StringKeyValue {
|
||||
l := iter.Len()
|
||||
if l == 0 {
|
||||
// TODO: That looks like a pointless allocation in case of
|
||||
// no labels, but returning nil from this function makes
|
||||
// the test fail.
|
||||
return []*commonpb.StringKeyValue{}
|
||||
}
|
||||
result := make([]*commonpb.StringKeyValue, 0, l)
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
result = append(result, &commonpb.StringKeyValue{
|
||||
Key: string(kv.Key),
|
||||
Value: kv.Value.Emit(),
|
||||
|
@ -69,7 +69,8 @@ func TestStringKeyValues(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.expected, stringKeyValues(test.kvs))
|
||||
iter := export.LabelSlice(test.kvs).Iter()
|
||||
assert.Equal(t, test.expected, stringKeyValues(iter))
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +150,7 @@ func TestMinMaxSumCountMetricDescriptor(t *testing.T) {
|
||||
metric.WithKeys(test.keys...),
|
||||
metric.WithDescription(test.description),
|
||||
metric.WithUnit(test.unit))
|
||||
labels := export.NewLabels(test.labels, "", nil)
|
||||
labels := export.NewLabels(export.LabelSlice(test.labels), "", nil)
|
||||
got, err := minMaxSumCount(&desc, labels, mmsc)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, test.expected, got.MetricDescriptor)
|
||||
@ -159,7 +160,7 @@ func TestMinMaxSumCountMetricDescriptor(t *testing.T) {
|
||||
|
||||
func TestMinMaxSumCountDatapoints(t *testing.T) {
|
||||
desc := metric.NewDescriptor("", metric.MeasureKind, core.Int64NumberKind)
|
||||
labels := export.NewLabels([]core.KeyValue{}, "", nil)
|
||||
labels := export.NewLabels(export.LabelSlice(nil), "", nil)
|
||||
mmsc := minmaxsumcount.New(&desc)
|
||||
assert.NoError(t, mmsc.Update(context.Background(), 1, &desc))
|
||||
assert.NoError(t, mmsc.Update(context.Background(), 10, &desc))
|
||||
@ -250,7 +251,7 @@ func TestSumMetricDescriptor(t *testing.T) {
|
||||
metric.WithDescription(test.description),
|
||||
metric.WithUnit(test.unit),
|
||||
)
|
||||
labels := export.NewLabels(test.labels, "", nil)
|
||||
labels := export.NewLabels(export.LabelSlice(test.labels), "", nil)
|
||||
got, err := sum(&desc, labels, sumAgg.New())
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, test.expected, got.MetricDescriptor)
|
||||
@ -260,7 +261,7 @@ func TestSumMetricDescriptor(t *testing.T) {
|
||||
|
||||
func TestSumInt64DataPoints(t *testing.T) {
|
||||
desc := metric.NewDescriptor("", metric.MeasureKind, core.Int64NumberKind)
|
||||
labels := export.NewLabels([]core.KeyValue{}, "", nil)
|
||||
labels := export.NewLabels(export.LabelSlice(nil), "", nil)
|
||||
s := sumAgg.New()
|
||||
assert.NoError(t, s.Update(context.Background(), core.Number(1), &desc))
|
||||
s.Checkpoint(context.Background(), &desc)
|
||||
@ -274,7 +275,7 @@ func TestSumInt64DataPoints(t *testing.T) {
|
||||
|
||||
func TestSumFloat64DataPoints(t *testing.T) {
|
||||
desc := metric.NewDescriptor("", metric.MeasureKind, core.Float64NumberKind)
|
||||
labels := export.NewLabels([]core.KeyValue{}, "", nil)
|
||||
labels := export.NewLabels(export.LabelSlice(nil), "", nil)
|
||||
s := sumAgg.New()
|
||||
assert.NoError(t, s.Update(context.Background(), core.NewFloat64Number(1), &desc))
|
||||
s.Checkpoint(context.Background(), &desc)
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metric
|
||||
package metric // import "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -169,6 +169,97 @@ type Exporter interface {
|
||||
Export(context.Context, CheckpointSet) error
|
||||
}
|
||||
|
||||
// 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 IteratorToSlice(iter LabelIterator) []core.KeyValue {
|
||||
l := iter.Len()
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
iter.idx = -1
|
||||
slice := make([]core.KeyValue, 0, l)
|
||||
for iter.Next() {
|
||||
slice = append(slice, iter.Label())
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// LabelStorage provides an access to the ordered labels.
|
||||
type LabelStorage interface {
|
||||
// NumLabels returns a number of labels in the storage.
|
||||
NumLabels() int
|
||||
// GetLabels gets a label from a passed index.
|
||||
GetLabel(int) core.KeyValue
|
||||
}
|
||||
|
||||
// LabelSlice implements LabelStorage in terms of a slice.
|
||||
type LabelSlice []core.KeyValue
|
||||
|
||||
var _ LabelStorage = LabelSlice{}
|
||||
|
||||
// NumLabels is a part of LabelStorage implementation.
|
||||
func (s LabelSlice) NumLabels() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// GetLabel is a part of LabelStorage implementation.
|
||||
func (s LabelSlice) GetLabel(idx int) core.KeyValue {
|
||||
return s[idx]
|
||||
}
|
||||
|
||||
// Iter returns an iterator going over the slice.
|
||||
func (s LabelSlice) Iter() LabelIterator {
|
||||
return NewLabelIterator(s)
|
||||
}
|
||||
|
||||
// LabelIterator allows iterating over an ordered set of labels. The
|
||||
// typical use of the iterator is as follows:
|
||||
//
|
||||
// iter := export.NewLabelIterator(getStorage())
|
||||
// for iter.Next() {
|
||||
// label := iter.Label()
|
||||
// // or, if we need an index:
|
||||
// // idx, label := iter.IndexedLabel()
|
||||
// // do something with label
|
||||
// }
|
||||
type LabelIterator struct {
|
||||
storage LabelStorage
|
||||
idx int
|
||||
}
|
||||
|
||||
// NewLabelIterator creates an iterator going over a passed storage.
|
||||
func NewLabelIterator(storage LabelStorage) LabelIterator {
|
||||
return LabelIterator{
|
||||
storage: storage,
|
||||
idx: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next moves the iterator to the next label. Returns false if there
|
||||
// are no more labels.
|
||||
func (i *LabelIterator) Next() bool {
|
||||
i.idx++
|
||||
return i.idx < i.Len()
|
||||
}
|
||||
|
||||
// Label returns current label. Must be called only after Next returns
|
||||
// true.
|
||||
func (i *LabelIterator) Label() core.KeyValue {
|
||||
return i.storage.GetLabel(i.idx)
|
||||
}
|
||||
|
||||
// IndexedLabel returns current index and label. Must be called only
|
||||
// after Next returns true.
|
||||
func (i *LabelIterator) IndexedLabel() (int, core.KeyValue) {
|
||||
return i.idx, i.Label()
|
||||
}
|
||||
|
||||
// Len returns a number of labels in the iterator's label storage.
|
||||
func (i *LabelIterator) Len() int {
|
||||
return i.storage.NumLabels()
|
||||
}
|
||||
|
||||
// LabelEncoder enables an optimization for export pipelines that use
|
||||
// text to encode their label sets.
|
||||
//
|
||||
@ -189,7 +280,7 @@ type LabelEncoder interface {
|
||||
// syntax for serialized label sets should implement
|
||||
// LabelEncoder, thus avoiding duplicate computation in the
|
||||
// export path.
|
||||
Encode([]core.KeyValue) string
|
||||
Encode(LabelIterator) string
|
||||
}
|
||||
|
||||
// CheckpointSet allows a controller to access a complete checkpoint of
|
||||
@ -221,7 +312,7 @@ type Record struct {
|
||||
// Batcher). If the batcher does not re-order labels, they are
|
||||
// presented in sorted order by the SDK.
|
||||
type Labels struct {
|
||||
ordered []core.KeyValue
|
||||
storage LabelStorage
|
||||
encoded string
|
||||
encoder LabelEncoder
|
||||
}
|
||||
@ -229,18 +320,17 @@ type Labels struct {
|
||||
// NewLabels builds a Labels object, consisting of an ordered set of
|
||||
// labels, a unique encoded representation, and the encoder that
|
||||
// produced it.
|
||||
func NewLabels(ordered []core.KeyValue, encoded string, encoder LabelEncoder) Labels {
|
||||
func NewLabels(storage LabelStorage, encoded string, encoder LabelEncoder) Labels {
|
||||
return Labels{
|
||||
ordered: ordered,
|
||||
storage: storage,
|
||||
encoded: encoded,
|
||||
encoder: encoder,
|
||||
}
|
||||
}
|
||||
|
||||
// Ordered returns the labels in a specified order, according to the
|
||||
// Batcher.
|
||||
func (l Labels) Ordered() []core.KeyValue {
|
||||
return l.ordered
|
||||
// Iter returns an iterator over ordered labels.
|
||||
func (l Labels) Iter() LabelIterator {
|
||||
return NewLabelIterator(l.storage)
|
||||
}
|
||||
|
||||
// Encoded is a pre-encoded form of the ordered labels.
|
||||
@ -253,11 +343,6 @@ func (l Labels) Encoder() LabelEncoder {
|
||||
return l.encoder
|
||||
}
|
||||
|
||||
// Len returns the number of labels.
|
||||
func (l Labels) Len() int {
|
||||
return len(l.ordered)
|
||||
}
|
||||
|
||||
// NewRecord allows Batcher implementations to construct export
|
||||
// records. The Descriptor, Labels, and Aggregator represent
|
||||
// aggregate metric events received over a single collection period.
|
||||
|
67
sdk/export/metric/metric_test.go
Normal file
67
sdk/export/metric/metric_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// 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 metric
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/key"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testSlice = []core.KeyValue{
|
||||
key.String("bar", "baz"),
|
||||
key.Int("foo", 42),
|
||||
}
|
||||
|
||||
func TestLabelIterator(t *testing.T) {
|
||||
iter := LabelSlice(testSlice).Iter()
|
||||
require.Equal(t, 2, iter.Len())
|
||||
|
||||
require.True(t, iter.Next())
|
||||
require.Equal(t, key.String("bar", "baz"), iter.Label())
|
||||
idx, label := iter.IndexedLabel()
|
||||
require.Equal(t, 0, idx)
|
||||
require.Equal(t, key.String("bar", "baz"), label)
|
||||
require.Equal(t, 2, iter.Len())
|
||||
|
||||
require.True(t, iter.Next())
|
||||
require.Equal(t, key.Int("foo", 42), iter.Label())
|
||||
idx, label = iter.IndexedLabel()
|
||||
require.Equal(t, 1, idx)
|
||||
require.Equal(t, key.Int("foo", 42), label)
|
||||
require.Equal(t, 2, iter.Len())
|
||||
|
||||
require.False(t, iter.Next())
|
||||
require.Equal(t, 2, iter.Len())
|
||||
}
|
||||
|
||||
func TestEmptyLabelIterator(t *testing.T) {
|
||||
iter := LabelSlice(nil).Iter()
|
||||
require.Equal(t, 0, iter.Len())
|
||||
require.False(t, iter.Next())
|
||||
}
|
||||
|
||||
func TestIteratorToSlice(t *testing.T) {
|
||||
iter := LabelSlice(testSlice).Iter()
|
||||
got := IteratorToSlice(iter)
|
||||
require.Equal(t, testSlice, got)
|
||||
|
||||
iter = LabelSlice(nil).Iter()
|
||||
got = IteratorToSlice(iter)
|
||||
require.Nil(t, got)
|
||||
}
|
@ -98,7 +98,9 @@ func (b *Batcher) Process(_ context.Context, record export.Record) error {
|
||||
// Note also the possibility to speed this computation of
|
||||
// "encoded" via "outputLabels" in the form of a (Descriptor,
|
||||
// LabelSet)->(Labels, Encoded) cache.
|
||||
for _, kv := range record.Labels().Ordered() {
|
||||
iter := record.Labels().Iter()
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
pos, ok := ki[kv.Key]
|
||||
if !ok {
|
||||
continue
|
||||
@ -107,7 +109,7 @@ func (b *Batcher) Process(_ context.Context, record export.Record) error {
|
||||
}
|
||||
|
||||
// Compute an encoded lookup key.
|
||||
encoded := b.labelEncoder.Encode(outputLabels)
|
||||
encoded := b.labelEncoder.Encode(export.LabelSlice(outputLabels).Iter())
|
||||
|
||||
// Merge this aggregator with all preceding aggregators that
|
||||
// map to the same set of `outputLabels` labels.
|
||||
@ -137,7 +139,7 @@ func (b *Batcher) Process(_ context.Context, record export.Record) error {
|
||||
}
|
||||
b.aggCheckpoint[key] = export.NewRecord(
|
||||
desc,
|
||||
export.NewLabels(outputLabels, encoded, b.labelEncoder),
|
||||
export.NewLabels(export.LabelSlice(outputLabels), encoded, b.labelEncoder),
|
||||
agg,
|
||||
)
|
||||
return nil
|
||||
|
@ -89,13 +89,14 @@ func (*testAggregationSelector) AggregatorFor(desc *metric.Descriptor) export.Ag
|
||||
}
|
||||
|
||||
func makeLabels(encoder export.LabelEncoder, labels ...core.KeyValue) export.Labels {
|
||||
encoded := encoder.Encode(labels)
|
||||
return export.NewLabels(labels, encoded, encoder)
|
||||
ls := export.LabelSlice(labels)
|
||||
return export.NewLabels(ls, encoder.Encode(ls.Iter()), encoder)
|
||||
}
|
||||
|
||||
func (Encoder) Encode(labels []core.KeyValue) string {
|
||||
func (Encoder) Encode(iter export.LabelIterator) string {
|
||||
var sb strings.Builder
|
||||
for i, l := range labels {
|
||||
for iter.Next() {
|
||||
i, l := iter.IndexedLabel()
|
||||
if i > 0 {
|
||||
sb.WriteString("&")
|
||||
}
|
||||
|
@ -38,16 +38,20 @@ type benchFixture struct {
|
||||
B *testing.B
|
||||
}
|
||||
|
||||
func newFixture(b *testing.B) *benchFixture {
|
||||
func newFixtureWithEncoder(b *testing.B, encoder export.LabelEncoder) *benchFixture {
|
||||
b.ReportAllocs()
|
||||
bf := &benchFixture{
|
||||
B: b,
|
||||
}
|
||||
bf.sdk = sdk.New(bf, sdk.NewDefaultLabelEncoder())
|
||||
bf.sdk = sdk.New(bf, encoder)
|
||||
bf.meter = metric.Must(metric.WrapMeterImpl(bf.sdk))
|
||||
return bf
|
||||
}
|
||||
|
||||
func newFixture(b *testing.B) *benchFixture {
|
||||
return newFixtureWithEncoder(b, sdk.NewDefaultLabelEncoder())
|
||||
}
|
||||
|
||||
func (*benchFixture) AggregatorFor(descriptor *metric.Descriptor) export.Aggregator {
|
||||
name := descriptor.Name()
|
||||
switch {
|
||||
@ -192,6 +196,68 @@ func BenchmarkAcquireReleaseExistingHandle(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// Iterators
|
||||
|
||||
type benchmarkEncoder struct {
|
||||
b *testing.B
|
||||
}
|
||||
|
||||
var _ export.LabelEncoder = benchmarkEncoder{}
|
||||
|
||||
var benchmarkEncoderVar core.KeyValue
|
||||
|
||||
func (e benchmarkEncoder) Encode(li export.LabelIterator) string {
|
||||
var kv core.KeyValue
|
||||
e.b.StartTimer()
|
||||
for i := 0; i < e.b.N; i++ {
|
||||
iter := li
|
||||
// test getting only the first element
|
||||
if iter.Next() {
|
||||
kv = iter.Label()
|
||||
}
|
||||
}
|
||||
e.b.StopTimer()
|
||||
benchmarkEncoderVar = kv
|
||||
return "foo=bar"
|
||||
}
|
||||
|
||||
func benchmarkIterator(b *testing.B, n int) {
|
||||
encoder := benchmarkEncoder{b: b}
|
||||
fix := newFixtureWithEncoder(b, encoder)
|
||||
labs := fix.sdk.Labels(makeLabels(n)...)
|
||||
cnt := fix.meter.NewInt64Counter("int64.counter")
|
||||
ctx := context.Background()
|
||||
cnt.Add(ctx, 1, labs)
|
||||
|
||||
b.StopTimer()
|
||||
b.ResetTimer()
|
||||
fix.sdk.Collect(ctx)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_0(b *testing.B) {
|
||||
benchmarkIterator(b, 0)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_1(b *testing.B) {
|
||||
benchmarkIterator(b, 1)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_2(b *testing.B) {
|
||||
benchmarkIterator(b, 2)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_4(b *testing.B) {
|
||||
benchmarkIterator(b, 4)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_8(b *testing.B) {
|
||||
benchmarkIterator(b, 8)
|
||||
}
|
||||
|
||||
func BenchmarkIterator_16(b *testing.B) {
|
||||
benchmarkIterator(b, 16)
|
||||
}
|
||||
|
||||
// Counters
|
||||
|
||||
func BenchmarkInt64CounterAdd(b *testing.B) {
|
||||
|
@ -104,7 +104,8 @@ func (b *testBatcher) FinishedCollection() {
|
||||
func (b *testBatcher) Process(_ context.Context, record export.Record) error {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
b.checkpointSet.Add(record.Descriptor(), record.Aggregator(), record.Labels().Ordered()...)
|
||||
labels := export.IteratorToSlice(record.Labels().Iter())
|
||||
b.checkpointSet.Add(record.Descriptor(), record.Aggregator(), labels...)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -69,8 +69,8 @@ func (cb *correctnessBatcher) Process(_ context.Context, record export.Record) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testLabelEncoder) Encode(labels []core.KeyValue) string {
|
||||
return fmt.Sprint(labels)
|
||||
func (testLabelEncoder) Encode(iter export.LabelIterator) string {
|
||||
return fmt.Sprint(export.IteratorToSlice(iter))
|
||||
}
|
||||
|
||||
func TestInputRangeTestCounter(t *testing.T) {
|
||||
@ -262,7 +262,8 @@ func TestSDKLabelsDeduplication(t *testing.T) {
|
||||
sum, _ := rec.Aggregator().(aggregator.Sum).Sum()
|
||||
require.Equal(t, sum, core.NewInt64Number(2))
|
||||
|
||||
actual = append(actual, rec.Labels().Ordered())
|
||||
kvs := export.IteratorToSlice(rec.Labels().Iter())
|
||||
actual = append(actual, kvs)
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, allExpect, actual)
|
||||
@ -271,18 +272,18 @@ func TestSDKLabelsDeduplication(t *testing.T) {
|
||||
func TestDefaultLabelEncoder(t *testing.T) {
|
||||
encoder := sdk.NewDefaultLabelEncoder()
|
||||
|
||||
encoded := encoder.Encode([]core.KeyValue{key.String("A", "B"), key.String("C", "D")})
|
||||
encoded := encoder.Encode(export.LabelSlice([]core.KeyValue{key.String("A", "B"), key.String("C", "D")}).Iter())
|
||||
require.Equal(t, `A=B,C=D`, encoded)
|
||||
|
||||
encoded = encoder.Encode([]core.KeyValue{key.String("A", "B,c=d"), key.String(`C\`, "D")})
|
||||
encoded = encoder.Encode(export.LabelSlice([]core.KeyValue{key.String("A", "B,c=d"), key.String(`C\`, "D")}).Iter())
|
||||
require.Equal(t, `A=B\,c\=d,C\\=D`, encoded)
|
||||
|
||||
encoded = encoder.Encode([]core.KeyValue{key.String(`\`, `=`), key.String(`,`, `\`)})
|
||||
encoded = encoder.Encode(export.LabelSlice([]core.KeyValue{key.String(`\`, `=`), key.String(`,`, `\`)}).Iter())
|
||||
require.Equal(t, `\\=\=,\,=\\`, encoded)
|
||||
|
||||
// Note: the label encoder does not sort or de-dup values,
|
||||
// that is done in Labels(...).
|
||||
encoded = encoder.Encode([]core.KeyValue{
|
||||
encoded = encoder.Encode(export.LabelSlice([]core.KeyValue{
|
||||
key.Int("I", 1),
|
||||
key.Uint("U", 1),
|
||||
key.Int32("I32", 1),
|
||||
@ -293,7 +294,7 @@ func TestDefaultLabelEncoder(t *testing.T) {
|
||||
key.Float64("F64", 1),
|
||||
key.String("S", "1"),
|
||||
key.Bool("B", true),
|
||||
})
|
||||
}).Iter())
|
||||
require.Equal(t, "I=1,U=1,I32=1,U32=1,I64=1,U64=1,F64=1,F64=1,S=1,B=true", encoded)
|
||||
}
|
||||
|
||||
|
@ -52,12 +52,13 @@ func NewDefaultLabelEncoder() export.LabelEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *defaultLabelEncoder) Encode(labels []core.KeyValue) string {
|
||||
func (d *defaultLabelEncoder) Encode(iter export.LabelIterator) string {
|
||||
buf := d.pool.Get().(*bytes.Buffer)
|
||||
defer d.pool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
for i, kv := range labels {
|
||||
for iter.Next() {
|
||||
i, kv := iter.IndexedLabel()
|
||||
if i > 0 {
|
||||
_, _ = buf.WriteRune(',')
|
||||
}
|
||||
|
@ -82,14 +82,18 @@ type (
|
||||
// repeatedly.
|
||||
labels struct {
|
||||
meter *SDK
|
||||
|
||||
// slice is a slice of `ordered`.
|
||||
slice sortedLabels
|
||||
|
||||
// ordered is the output of sorting and deduplicating
|
||||
// the labels, copied into an array of the correct
|
||||
// size for use as a map key.
|
||||
ordered orderedLabels
|
||||
|
||||
// sortSlice has a single purpose - as a temporary
|
||||
// place for sorting during labels creation to avoid
|
||||
// allocation
|
||||
sortSlice sortedLabels
|
||||
// cachedValue contains a `reflect.Value` of the `ordered`
|
||||
// member
|
||||
cachedValue reflect.Value
|
||||
}
|
||||
|
||||
// mapkey uniquely describes a metric instrument in terms of
|
||||
@ -150,11 +154,12 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
_ api.MeterImpl = &SDK{}
|
||||
_ api.LabelSet = &labels{}
|
||||
_ api.AsyncImpl = &asyncInstrument{}
|
||||
_ api.SyncImpl = &syncInstrument{}
|
||||
_ api.BoundSyncImpl = &record{}
|
||||
_ api.MeterImpl = &SDK{}
|
||||
_ api.LabelSet = &labels{}
|
||||
_ api.AsyncImpl = &asyncInstrument{}
|
||||
_ api.SyncImpl = &syncInstrument{}
|
||||
_ api.BoundSyncImpl = &record{}
|
||||
_ export.LabelStorage = &labels{}
|
||||
|
||||
kvType = reflect.TypeOf(core.KeyValue{})
|
||||
)
|
||||
@ -295,11 +300,15 @@ func (s *syncInstrument) RecordOne(ctx context.Context, number core.Number, ls a
|
||||
// own periodic collection.
|
||||
func New(batcher export.Batcher, labelEncoder export.LabelEncoder) *SDK {
|
||||
m := &SDK{
|
||||
empty: labels{
|
||||
ordered: [0]core.KeyValue{},
|
||||
},
|
||||
batcher: batcher,
|
||||
labelEncoder: labelEncoder,
|
||||
errorHandler: DefaultErrorHandler,
|
||||
}
|
||||
m.empty.meter = m
|
||||
m.empty.cachedValue = reflect.ValueOf(m.empty.ordered)
|
||||
return m
|
||||
}
|
||||
|
||||
@ -307,8 +316,10 @@ func DefaultErrorHandler(err error) {
|
||||
fmt.Fprintln(os.Stderr, "Metrics SDK error:", err)
|
||||
}
|
||||
|
||||
// Labels returns a LabelSet corresponding to the arguments. Passed
|
||||
// labels are de-duplicated, with last-value-wins semantics.
|
||||
// Labels returns a LabelSet corresponding to the arguments. Passed labels
|
||||
// are sorted and de-duplicated, with last-value-wins semantics. Note that
|
||||
// sorting and deduplicating happens in-place to avoid allocation, so the
|
||||
// passed slice will be modified.
|
||||
func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
||||
// Check for empty set.
|
||||
if len(kvs) == 0 {
|
||||
@ -316,33 +327,15 @@ func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
||||
}
|
||||
|
||||
ls := &labels{ // allocation
|
||||
meter: m,
|
||||
slice: kvs,
|
||||
meter: m,
|
||||
sortSlice: kvs,
|
||||
}
|
||||
|
||||
// Sort and de-duplicate. Note: this use of `ls.slice` avoids
|
||||
// an allocation by using the address-able field rather than
|
||||
// `kvs`. Labels retains a copy of this slice, i.e., the
|
||||
// initial allocation at the varargs call site.
|
||||
//
|
||||
// Note that `ls.slice` continues to refer to this memory,
|
||||
// even though a new array is allocated for `ls.ordered`. It
|
||||
// is possible for the `slice` to refer to the same memory,
|
||||
// although in the reflection code path of `computeOrdered` it
|
||||
// costs an allocation to yield a slice through
|
||||
// `(reflect.Value).Interface()`.
|
||||
//
|
||||
// TODO: There is a possibility that the caller passes values
|
||||
// without an allocation (e.g., `meter.Labels(kvs...)`), and
|
||||
// that the user could later modify the slice, leading to
|
||||
// incorrect results. This is indeed a risk, one that should
|
||||
// be quickly addressed via the following TODO.
|
||||
//
|
||||
// TODO: It would be better overall if the export.Labels interface
|
||||
// did not expose a slice via `Ordered()`, if instead it exposed
|
||||
// getter methods like `Len()` and `Order(i int)`. Then we would
|
||||
// just implement the interface using the `orderedLabels` array.
|
||||
sort.Stable(&ls.slice)
|
||||
// Sort and de-duplicate. Note: this use of `ls.sortSlice`
|
||||
// avoids an allocation by using the address-able field rather
|
||||
// than `kvs`.
|
||||
sort.Stable(&ls.sortSlice)
|
||||
ls.sortSlice = nil
|
||||
|
||||
oi := 1
|
||||
for i := 1; i < len(kvs); i++ {
|
||||
@ -355,64 +348,81 @@ func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
||||
oi++
|
||||
}
|
||||
kvs = kvs[0:oi]
|
||||
ls.slice = kvs
|
||||
ls.computeOrdered(kvs)
|
||||
return ls
|
||||
}
|
||||
|
||||
func (ls *labels) NumLabels() int {
|
||||
return ls.cachedValue.Len()
|
||||
}
|
||||
|
||||
func (ls *labels) GetLabel(idx int) core.KeyValue {
|
||||
return ls.cachedValue.Index(idx).Interface().(core.KeyValue)
|
||||
}
|
||||
|
||||
func (ls *labels) computeOrdered(kvs []core.KeyValue) {
|
||||
ls.ordered = computeOrderedFixed(kvs)
|
||||
if ls.ordered == nil {
|
||||
ls.ordered = computeOrderedReflect(kvs)
|
||||
}
|
||||
ls.cachedValue = reflect.ValueOf(ls.ordered)
|
||||
}
|
||||
|
||||
func computeOrderedFixed(kvs []core.KeyValue) orderedLabels {
|
||||
switch len(kvs) {
|
||||
case 1:
|
||||
ptr := new([1]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 2:
|
||||
ptr := new([2]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 3:
|
||||
ptr := new([3]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 4:
|
||||
ptr := new([4]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 5:
|
||||
ptr := new([5]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 6:
|
||||
ptr := new([6]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 7:
|
||||
ptr := new([7]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 8:
|
||||
ptr := new([8]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 9:
|
||||
ptr := new([9]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
case 10:
|
||||
ptr := new([10]core.KeyValue)
|
||||
copy((*ptr)[:], kvs)
|
||||
ls.ordered = *ptr
|
||||
return *ptr
|
||||
default:
|
||||
at := reflect.New(reflect.ArrayOf(len(kvs), kvType)).Elem()
|
||||
|
||||
for i := 0; i < len(kvs); i++ {
|
||||
*(at.Index(i).Addr().Interface().(*core.KeyValue)) = kvs[i]
|
||||
}
|
||||
|
||||
ls.ordered = at.Interface()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func computeOrderedReflect(kvs []core.KeyValue) interface{} {
|
||||
at := reflect.New(reflect.ArrayOf(len(kvs), kvType)).Elem()
|
||||
for i, kv := range kvs {
|
||||
*(at.Index(i).Addr().Interface().(*core.KeyValue)) = kv
|
||||
}
|
||||
return at.Interface()
|
||||
}
|
||||
|
||||
// labsFor sanitizes the input LabelSet. The input will be rejected
|
||||
// if it was created by another Meter instance, for example.
|
||||
func (m *SDK) labsFor(ls api.LabelSet) *labels {
|
||||
@ -539,7 +549,8 @@ func (m *SDK) checkpoint(ctx context.Context, descriptor *metric.Descriptor, rec
|
||||
// instead of once per bound instrument lifetime. This can be
|
||||
// addressed similarly to OTEP 78, see
|
||||
// https://github.com/jmacd/opentelemetry-go/blob/8bed2e14df7f9f4688fbab141924bb786dc9a3a1/api/context/internal/set.go#L89
|
||||
exportLabels := export.NewLabels(labels.slice, m.labelEncoder.Encode(labels.slice), m.labelEncoder)
|
||||
iter := export.NewLabelIterator(labels)
|
||||
exportLabels := export.NewLabels(labels, m.labelEncoder.Encode(iter), m.labelEncoder)
|
||||
exportRecord := export.NewRecord(descriptor, exportLabels, recorder)
|
||||
err := m.batcher.Process(ctx, exportRecord)
|
||||
if err != nil {
|
||||
|
@ -250,8 +250,9 @@ func (*testFixture) FinishedCollection() {
|
||||
}
|
||||
|
||||
func (f *testFixture) Process(_ context.Context, record export.Record) error {
|
||||
labels := export.IteratorToSlice(record.Labels().Iter())
|
||||
key := testKey{
|
||||
labels: canonicalizeLabels(record.Labels().Ordered()),
|
||||
labels: canonicalizeLabels(labels),
|
||||
descriptor: record.Descriptor(),
|
||||
}
|
||||
if f.dupCheck[key] == 0 {
|
||||
|
Loading…
Reference in New Issue
Block a user