You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
Replace recordingSpan attributes implementation with slice of attributes (#2576)
* Replace recordingSpan attributes implementation Instead of an LRU strategy for cap-ing span attributes, comply with the specification and drop last added. To do this, the attributesmap is replaced with a slice of attributes. * Remove attributesmap files * Refine addition algorithm Unify duplicated code. Fix deduplication algorithm. Fix droppedAttributes to always be returned, even if the span has no attributes. * Unify span SetAttributes tests * Doc fix to attr drop order in changelog * Test span and snapshot attrs * fix lint * Add tests for recordingSpan method defaults * Comment why pre-allocation is not done * Correct grammar in recordingSpan allocation comment * Update sdk/trace/tracer.go Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
This commit is contained in:
@@ -31,6 +31,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
- Specify explicit buckets in Prometheus example. (#2493)
|
||||
- W3C baggage will now decode urlescaped values. (#2529)
|
||||
- Baggage members are now only validated once, when calling `NewMember` and not also when adding it to the baggage itself. (#2522)
|
||||
- The order attributes are dropped from spans in the `go.opentelemetry.io/otel/sdk/trace` package when capacity is reached is fixed to be in compliance with the OpenTelemetry specification.
|
||||
Instead of dropping the least-recently-used attribute, the last added attribute is dropped.
|
||||
This drop order still only applies to attributes with unique keys not already contained in the span.
|
||||
If an attribute is added with a key already contained in the span, that attribute is updated to the new value being added. (#2576)
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
// 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 trace // import "go.opentelemetry.io/otel/sdk/trace"
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// attributesMap is a capped map of attributes, holding the most recent attributes.
|
||||
// Eviction is done via a LRU method, the oldest entry is removed to create room for a new entry.
|
||||
// Updates are allowed and they refresh the usage of the key.
|
||||
//
|
||||
// This is based from https://github.com/hashicorp/golang-lru/blob/master/simplelru/lru.go
|
||||
// With a subset of the its operations and specific for holding attribute.KeyValue
|
||||
type attributesMap struct {
|
||||
attributes map[attribute.Key]*list.Element
|
||||
evictList *list.List
|
||||
droppedCount int
|
||||
capacity int
|
||||
}
|
||||
|
||||
func newAttributesMap(capacity int) *attributesMap {
|
||||
lm := &attributesMap{
|
||||
attributes: make(map[attribute.Key]*list.Element),
|
||||
evictList: list.New(),
|
||||
capacity: capacity,
|
||||
}
|
||||
return lm
|
||||
}
|
||||
|
||||
func (am *attributesMap) add(kv attribute.KeyValue) {
|
||||
// Check for existing item
|
||||
if ent, ok := am.attributes[kv.Key]; ok {
|
||||
am.evictList.MoveToFront(ent)
|
||||
ent.Value = &kv
|
||||
return
|
||||
}
|
||||
|
||||
// Add new item
|
||||
entry := am.evictList.PushFront(&kv)
|
||||
am.attributes[kv.Key] = entry
|
||||
|
||||
// Verify size not exceeded
|
||||
if am.evictList.Len() > am.capacity {
|
||||
am.removeOldest()
|
||||
am.droppedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// toKeyValue copies the attributesMap into a slice of attribute.KeyValue and
|
||||
// returns it. If the map is empty, a nil is returned.
|
||||
// TODO: Is it more efficient to return a pointer to the slice?
|
||||
func (am *attributesMap) toKeyValue() []attribute.KeyValue {
|
||||
len := am.evictList.Len()
|
||||
if len == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attributes := make([]attribute.KeyValue, 0, len)
|
||||
for ent := am.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
if value, ok := ent.Value.(*attribute.KeyValue); ok {
|
||||
attributes = append(attributes, *value)
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (am *attributesMap) removeOldest() {
|
||||
ent := am.evictList.Back()
|
||||
if ent != nil {
|
||||
am.evictList.Remove(ent)
|
||||
kv := ent.Value.(*attribute.KeyValue)
|
||||
delete(am.attributes, kv.Key)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// 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 trace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
const testKeyFmt = "test-key-%d"
|
||||
|
||||
func TestAttributesMap(t *testing.T) {
|
||||
wantCapacity := 128
|
||||
attrMap := newAttributesMap(wantCapacity)
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
attrMap.add(attribute.Int(fmt.Sprintf(testKeyFmt, i), i))
|
||||
}
|
||||
if attrMap.capacity != wantCapacity {
|
||||
t.Errorf("attrMap.capacity: got '%d'; want '%d'", attrMap.capacity, wantCapacity)
|
||||
}
|
||||
|
||||
if attrMap.droppedCount != wantCapacity {
|
||||
t.Errorf("attrMap.droppedCount: got '%d'; want '%d'", attrMap.droppedCount, wantCapacity)
|
||||
}
|
||||
|
||||
for i := 0; i < wantCapacity; i++ {
|
||||
key := attribute.Key(fmt.Sprintf(testKeyFmt, i))
|
||||
_, ok := attrMap.attributes[key]
|
||||
if ok {
|
||||
t.Errorf("key %q should be dropped", testKeyFmt)
|
||||
}
|
||||
}
|
||||
for i := wantCapacity; i < 256; i++ {
|
||||
key := attribute.Key(fmt.Sprintf(testKeyFmt, i))
|
||||
_, ok := attrMap.attributes[key]
|
||||
if !ok {
|
||||
t.Errorf("key %q should not be dropped", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributesMapGetOldestRemoveOldest(t *testing.T) {
|
||||
attrMap := newAttributesMap(128)
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
attrMap.add(attribute.Int(fmt.Sprintf(testKeyFmt, i), i))
|
||||
}
|
||||
|
||||
attrMap.removeOldest()
|
||||
attrMap.removeOldest()
|
||||
attrMap.removeOldest()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
key := attribute.Key(fmt.Sprintf(testKeyFmt, i))
|
||||
_, ok := attrMap.attributes[key]
|
||||
if ok {
|
||||
t.Errorf("key %q should be removed", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributesMapToKeyValue(t *testing.T) {
|
||||
attrMap := newAttributesMap(128)
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
attrMap.add(attribute.Int(fmt.Sprintf(testKeyFmt, i), i))
|
||||
}
|
||||
|
||||
kv := attrMap.toKeyValue()
|
||||
|
||||
gotAttrLen := len(kv)
|
||||
wantAttrLen := 128
|
||||
if gotAttrLen != wantAttrLen {
|
||||
t.Errorf("len(attrMap.attributes): got '%d'; want '%d'", gotAttrLen, wantAttrLen)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAttributesMapToKeyValue(b *testing.B) {
|
||||
attrMap := newAttributesMap(128)
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
attrMap.add(attribute.Int(fmt.Sprintf(testKeyFmt, i), i))
|
||||
}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
attrMap.toKeyValue()
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package trace_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +25,28 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func BenchmarkSpanSetAttributesOverCapacity(b *testing.B) {
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSpanLimits(sdktrace.SpanLimits{AttributeCountLimit: 1}),
|
||||
)
|
||||
tracer := tp.Tracer("BenchmarkSpanSetAttributesOverCapacity")
|
||||
ctx := context.Background()
|
||||
attrs := make([]attribute.KeyValue, 128)
|
||||
for i := range attrs {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
attrs[i] = attribute.Bool(key, true)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, span := tracer.Start(ctx, "/foo")
|
||||
span.SetAttributes(attrs...)
|
||||
span.End()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStartEndSpan(b *testing.B) {
|
||||
traceBenchmark(b, "Benchmark StartEndSpan", func(b *testing.B, t trace.Tracer) {
|
||||
ctx := context.Background()
|
||||
|
||||
+119
-23
@@ -54,6 +54,7 @@ type ReadOnlySpan interface {
|
||||
// the span has not ended.
|
||||
EndTime() time.Time
|
||||
// Attributes returns the defining attributes of the span.
|
||||
// The order of the returned attributes is not guaranteed to be stable across invocations.
|
||||
Attributes() []attribute.KeyValue
|
||||
// Links returns all the links the span has to other spans.
|
||||
Links() []Link
|
||||
@@ -129,9 +130,14 @@ type recordingSpan struct {
|
||||
// spanContext holds the SpanContext of this span.
|
||||
spanContext trace.SpanContext
|
||||
|
||||
// attributes are capped at configured limit. When the capacity is reached
|
||||
// an oldest entry is removed to create room for a new entry.
|
||||
attributes *attributesMap
|
||||
// attributes is a collection of user provided key/values. The collection
|
||||
// is constrained by a configurable maximum held by the parent
|
||||
// TracerProvider. When additional attributes are added after this maximum
|
||||
// is reached these attributes the user is attempting to add are dropped.
|
||||
// This dropped number of attributes is tracked and reported in the
|
||||
// ReadOnlySpan exported when the span ends.
|
||||
attributes []attribute.KeyValue
|
||||
droppedAttributes int
|
||||
|
||||
// events are stored in FIFO queue capped by configured limit.
|
||||
events evictedQueue
|
||||
@@ -194,11 +200,80 @@ func (s *recordingSpan) SetStatus(code codes.Code, description string) {
|
||||
// will be overwritten with the value contained in attributes.
|
||||
//
|
||||
// If this span is not being recorded than this method does nothing.
|
||||
//
|
||||
// If adding attributes to the span would exceed the maximum amount of
|
||||
// attributes the span is configured to have, the last added attributes will
|
||||
// be dropped.
|
||||
func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) {
|
||||
if !s.IsRecording() {
|
||||
return
|
||||
}
|
||||
s.copyToCappedAttributes(attributes...)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// If adding these attributes could exceed the capacity of s perform a
|
||||
// de-duplication and truncation while adding to avoid over allocation.
|
||||
if len(s.attributes)+len(attributes) > s.tracer.provider.spanLimits.AttributeCountLimit {
|
||||
s.addOverCapAttrs(attributes)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, add without deduplication. When attributes are read they
|
||||
// will be deduplicated, optimizing the operation.
|
||||
for _, a := range attributes {
|
||||
if !a.Valid() {
|
||||
// Drop all invalid attributes.
|
||||
s.droppedAttributes++
|
||||
continue
|
||||
}
|
||||
s.attributes = append(s.attributes, a)
|
||||
}
|
||||
}
|
||||
|
||||
// addOverCapAttrs adds the attributes attrs to the span s while
|
||||
// de-duplicating the attributes of s and attrs and dropping attributes that
|
||||
// exceed the capacity of s.
|
||||
//
|
||||
// This method assumes s.mu.Lock is held by the caller.
|
||||
//
|
||||
// This method should only be called when there is a possibility that adding
|
||||
// attrs to s will exceed the capacity of s. Otherwise, attrs should be added
|
||||
// to s without checking for duplicates and all retrieval methods of the
|
||||
// attributes for s will de-duplicate as needed.
|
||||
func (s *recordingSpan) addOverCapAttrs(attrs []attribute.KeyValue) {
|
||||
// In order to not allocate more capacity to s.attributes than needed,
|
||||
// prune and truncate this addition of attributes while adding.
|
||||
|
||||
// Do not set a capacity when creating this map. Benchmark testing has
|
||||
// showed this to only add unused memory allocations in general use.
|
||||
exists := make(map[attribute.Key]int)
|
||||
s.dedupeAttrsFromRecord(&exists)
|
||||
|
||||
// Now that s.attributes is deduplicated, adding unique attributes up to
|
||||
// the capacity of s will not over allocate s.attributes.
|
||||
for _, a := range attrs {
|
||||
if !a.Valid() {
|
||||
// Drop all invalid attributes.
|
||||
s.droppedAttributes++
|
||||
continue
|
||||
}
|
||||
|
||||
if idx, ok := exists[a.Key]; ok {
|
||||
// Perform all updates before dropping, even when at capacity.
|
||||
s.attributes[idx] = a
|
||||
continue
|
||||
}
|
||||
|
||||
if len(s.attributes) >= s.tracer.provider.spanLimits.AttributeCountLimit {
|
||||
// Do not just drop all of the remaining attributes, make sure
|
||||
// updates are checked and performed.
|
||||
s.droppedAttributes++
|
||||
} else {
|
||||
s.attributes = append(s.attributes, a)
|
||||
exists[a.Key] = len(s.attributes) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End ends the span. This method does nothing if the span is already ended or
|
||||
@@ -388,13 +463,45 @@ func (s *recordingSpan) EndTime() time.Time {
|
||||
}
|
||||
|
||||
// Attributes returns the attributes of this span.
|
||||
//
|
||||
// The order of the returned attributes is not guaranteed to be stable.
|
||||
func (s *recordingSpan) Attributes() []attribute.KeyValue {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.attributes.evictList.Len() == 0 {
|
||||
return []attribute.KeyValue{}
|
||||
s.dedupeAttrs()
|
||||
return s.attributes
|
||||
}
|
||||
|
||||
// dedupeAttrs deduplicates the attributes of s to fit capacity.
|
||||
//
|
||||
// This method assumes s.mu.Lock is held by the caller.
|
||||
func (s *recordingSpan) dedupeAttrs() {
|
||||
// Do not set a capacity when creating this map. Benchmark testing has
|
||||
// showed this to only add unused memory allocations in general use.
|
||||
exists := make(map[attribute.Key]int)
|
||||
s.dedupeAttrsFromRecord(&exists)
|
||||
}
|
||||
|
||||
// dedupeAttrsFromRecord deduplicates the attributes of s to fit capacity
|
||||
// using record as the record of unique attribute keys to their index.
|
||||
//
|
||||
// This method assumes s.mu.Lock is held by the caller.
|
||||
func (s *recordingSpan) dedupeAttrsFromRecord(record *map[attribute.Key]int) {
|
||||
// Use the fact that slices share the same backing array.
|
||||
unique := s.attributes[:0]
|
||||
for _, a := range s.attributes {
|
||||
if idx, ok := (*record)[a.Key]; ok {
|
||||
unique[idx] = a
|
||||
} else {
|
||||
unique = append(unique, a)
|
||||
(*record)[a.Key] = len(unique) - 1
|
||||
}
|
||||
}
|
||||
return s.attributes.toKeyValue()
|
||||
// s.attributes have element types of attribute.KeyValue. These types are
|
||||
// not pointers and they themselves do not contain pointer fields,
|
||||
// therefore the duplicate values do not need to be zeroed for them to be
|
||||
// garbage collected.
|
||||
s.attributes = unique
|
||||
}
|
||||
|
||||
// Links returns the links of this span.
|
||||
@@ -463,7 +570,7 @@ func (s *recordingSpan) addLink(link trace.Link) {
|
||||
func (s *recordingSpan) DroppedAttributes() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.attributes.droppedCount
|
||||
return s.droppedAttributes
|
||||
}
|
||||
|
||||
// DroppedLinks returns the number of links dropped by the span due to limits
|
||||
@@ -513,10 +620,11 @@ func (s *recordingSpan) snapshot() ReadOnlySpan {
|
||||
sd.status = s.status
|
||||
sd.childSpanCount = s.childSpanCount
|
||||
|
||||
if s.attributes.evictList.Len() > 0 {
|
||||
sd.attributes = s.attributes.toKeyValue()
|
||||
sd.droppedAttributeCount = s.attributes.droppedCount
|
||||
if len(s.attributes) > 0 {
|
||||
s.dedupeAttrs()
|
||||
sd.attributes = s.attributes
|
||||
}
|
||||
sd.droppedAttributeCount = s.droppedAttributes
|
||||
if len(s.events.queue) > 0 {
|
||||
sd.events = s.interfaceArrayToEventArray()
|
||||
sd.droppedEventCount = s.events.droppedCount
|
||||
@@ -544,18 +652,6 @@ func (s *recordingSpan) interfaceArrayToEventArray() []Event {
|
||||
return eventArr
|
||||
}
|
||||
|
||||
func (s *recordingSpan) copyToCappedAttributes(attributes ...attribute.KeyValue) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, a := range attributes {
|
||||
// Ensure attributes conform to the specification:
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.0.1/specification/common/common.md#attributes
|
||||
if a.Valid() {
|
||||
s.attributes.add(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *recordingSpan) addChild() {
|
||||
if !s.IsRecording() {
|
||||
return
|
||||
|
||||
+189
-88
@@ -419,34 +419,6 @@ func TestSetSpanAttributesOnStart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSpanAttributes(t *testing.T) {
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
|
||||
span := startSpan(tp, "SpanAttribute")
|
||||
span.SetAttributes(attribute.String("key1", "value1"))
|
||||
got, err := endSpan(te, span)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := &snapshot{
|
||||
spanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: tid,
|
||||
TraceFlags: 0x1,
|
||||
}),
|
||||
parent: sc.WithRemote(true),
|
||||
name: "span0",
|
||||
attributes: []attribute.KeyValue{
|
||||
attribute.String("key1", "value1"),
|
||||
},
|
||||
spanKind: trace.SpanKindInternal,
|
||||
instrumentationLibrary: instrumentation.Library{Name: "SpanAttribute"},
|
||||
}
|
||||
if diff := cmpDiff(got, want); diff != "" {
|
||||
t.Errorf("SetSpanAttributes: -got +want %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSamplerAttributesLocalChildSpan(t *testing.T) {
|
||||
sampler := &testSampler{prefix: "span", t: t}
|
||||
te := NewTestExporter()
|
||||
@@ -469,72 +441,193 @@ func TestSamplerAttributesLocalChildSpan(t *testing.T) {
|
||||
assert.Equal(t, []attribute.KeyValue{attribute.Int("callCount", 1)}, gotSpan1.Attributes())
|
||||
}
|
||||
|
||||
func TestSetSpanAttributesOverLimit(t *testing.T) {
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(WithSpanLimits(SpanLimits{AttributeCountLimit: 2}), WithSyncer(te), WithResource(resource.Empty()))
|
||||
|
||||
span := startSpan(tp, "SpanAttributesOverLimit")
|
||||
span.SetAttributes(
|
||||
attribute.Bool("key1", true),
|
||||
func TestSpanSetAttributes(t *testing.T) {
|
||||
attrs := [...]attribute.KeyValue{
|
||||
attribute.String("key1", "value1"),
|
||||
attribute.String("key2", "value2"),
|
||||
attribute.Bool("key1", false), // Replace key1.
|
||||
attribute.Int64("key4", 4), // Remove key2 and add key4
|
||||
)
|
||||
got, err := endSpan(te, span)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
attribute.String("key3", "value3"),
|
||||
attribute.String("key4", "value4"),
|
||||
attribute.String("key1", "value5"),
|
||||
attribute.String("key2", "value6"),
|
||||
attribute.String("key3", "value7"),
|
||||
}
|
||||
invalid := attribute.KeyValue{}
|
||||
|
||||
want := &snapshot{
|
||||
spanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: tid,
|
||||
TraceFlags: 0x1,
|
||||
}),
|
||||
parent: sc.WithRemote(true),
|
||||
name: "span0",
|
||||
attributes: []attribute.KeyValue{
|
||||
attribute.Bool("key1", false),
|
||||
attribute.Int64("key4", 4),
|
||||
tests := []struct {
|
||||
name string
|
||||
input [][]attribute.KeyValue
|
||||
wantAttrs []attribute.KeyValue
|
||||
wantDropped int
|
||||
}{
|
||||
{
|
||||
name: "array",
|
||||
input: [][]attribute.KeyValue{attrs[:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
spanKind: trace.SpanKindInternal,
|
||||
droppedAttributeCount: 1,
|
||||
instrumentationLibrary: instrumentation.Library{Name: "SpanAttributesOverLimit"},
|
||||
}
|
||||
if diff := cmpDiff(got, want); diff != "" {
|
||||
t.Errorf("SetSpanAttributesOverLimit: -got +want %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSpanAttributesWithInvalidKey(t *testing.T) {
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(WithSpanLimits(SpanLimits{}), WithSyncer(te), WithResource(resource.Empty()))
|
||||
|
||||
span := startSpan(tp, "SpanToSetInvalidKeyOrValue")
|
||||
span.SetAttributes(
|
||||
attribute.Bool("", true),
|
||||
attribute.Bool("key1", false),
|
||||
)
|
||||
got, err := endSpan(te, span)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := &snapshot{
|
||||
spanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: tid,
|
||||
TraceFlags: 0x1,
|
||||
}),
|
||||
parent: sc.WithRemote(true),
|
||||
name: "span0",
|
||||
attributes: []attribute.KeyValue{
|
||||
attribute.Bool("key1", false),
|
||||
{
|
||||
name: "single_value:array",
|
||||
input: [][]attribute.KeyValue{attrs[:1], attrs[1:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "array:single_value",
|
||||
input: [][]attribute.KeyValue{attrs[:2], attrs[2:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "single_values",
|
||||
input: [][]attribute.KeyValue{attrs[:1], attrs[1:2], attrs[2:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
|
||||
// The tracing specification states:
|
||||
//
|
||||
// For each unique attribute key, addition of which would result in
|
||||
// exceeding the limit, SDK MUST discard that key/value pair
|
||||
//
|
||||
// Therefore, adding attributes after the capacity is reached should
|
||||
// result in those attributes being dropped.
|
||||
|
||||
{
|
||||
name: "drop_last_added",
|
||||
input: [][]attribute.KeyValue{attrs[:3], attrs[3:4], attrs[3:4]},
|
||||
wantAttrs: attrs[:3],
|
||||
wantDropped: 2,
|
||||
},
|
||||
|
||||
// The tracing specification states:
|
||||
//
|
||||
// Setting an attribute with the same key as an existing attribute
|
||||
// SHOULD overwrite the existing attribute's value.
|
||||
//
|
||||
// Therefore, attributes are updated regardless of capacity state.
|
||||
|
||||
{
|
||||
name: "single_value_update",
|
||||
input: [][]attribute.KeyValue{attrs[:1], attrs[:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "all_update",
|
||||
input: [][]attribute.KeyValue{attrs[:3], attrs[4:7]},
|
||||
wantAttrs: attrs[4:7],
|
||||
},
|
||||
{
|
||||
name: "all_update/multi",
|
||||
input: [][]attribute.KeyValue{attrs[:3], attrs[4:7], attrs[:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "deduplicate/under_capacity",
|
||||
input: [][]attribute.KeyValue{attrs[:1], attrs[:1], attrs[:1]},
|
||||
wantAttrs: attrs[:1],
|
||||
},
|
||||
{
|
||||
name: "deduplicate/over_capacity",
|
||||
input: [][]attribute.KeyValue{attrs[:1], attrs[:1], attrs[:1], attrs[:3]},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "deduplicate/added",
|
||||
input: [][]attribute.KeyValue{
|
||||
attrs[:2],
|
||||
{attrs[2], attrs[2], attrs[2]},
|
||||
},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "deduplicate/added_at_cappacity",
|
||||
input: [][]attribute.KeyValue{
|
||||
attrs[:3],
|
||||
{attrs[2], attrs[2], attrs[2]},
|
||||
},
|
||||
wantAttrs: attrs[:3],
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
input: [][]attribute.KeyValue{
|
||||
{invalid},
|
||||
},
|
||||
wantDropped: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid_with_valid",
|
||||
input: [][]attribute.KeyValue{
|
||||
{invalid, attrs[0]},
|
||||
},
|
||||
wantAttrs: attrs[:1],
|
||||
wantDropped: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid_over_capacity",
|
||||
input: [][]attribute.KeyValue{
|
||||
{invalid, invalid, invalid, invalid, attrs[0]},
|
||||
},
|
||||
wantAttrs: attrs[:1],
|
||||
wantDropped: 4,
|
||||
},
|
||||
{
|
||||
name: "valid:invalid/under_capacity",
|
||||
input: [][]attribute.KeyValue{
|
||||
attrs[:1],
|
||||
{invalid},
|
||||
},
|
||||
wantAttrs: attrs[:1],
|
||||
wantDropped: 1,
|
||||
},
|
||||
{
|
||||
name: "valid:invalid/over_capacity",
|
||||
input: [][]attribute.KeyValue{
|
||||
attrs[:1],
|
||||
{invalid, invalid, invalid, invalid},
|
||||
},
|
||||
wantAttrs: attrs[:1],
|
||||
wantDropped: 4,
|
||||
},
|
||||
{
|
||||
name: "valid_at_capacity:invalid",
|
||||
input: [][]attribute.KeyValue{
|
||||
attrs[:3],
|
||||
{invalid, invalid, invalid, invalid},
|
||||
},
|
||||
wantAttrs: attrs[:3],
|
||||
wantDropped: 4,
|
||||
},
|
||||
spanKind: trace.SpanKindInternal,
|
||||
droppedAttributeCount: 0,
|
||||
instrumentationLibrary: instrumentation.Library{Name: "SpanToSetInvalidKeyOrValue"},
|
||||
}
|
||||
if diff := cmpDiff(got, want); diff != "" {
|
||||
t.Errorf("SetSpanAttributesWithInvalidKey: -got +want %s", diff)
|
||||
|
||||
const (
|
||||
capacity = 3
|
||||
instName = "TestSpanAttributeCapacity"
|
||||
spanName = "test span"
|
||||
)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(
|
||||
WithSyncer(te),
|
||||
WithSpanLimits(SpanLimits{AttributeCountLimit: capacity}),
|
||||
)
|
||||
_, span := tp.Tracer(instName).Start(context.Background(), spanName)
|
||||
for _, a := range test.input {
|
||||
span.SetAttributes(a...)
|
||||
}
|
||||
span.End()
|
||||
|
||||
require.Implements(t, (*ReadOnlySpan)(nil), span)
|
||||
roSpan := span.(ReadOnlySpan)
|
||||
|
||||
// Ensure the span itself is valid.
|
||||
assert.ElementsMatch(t, test.wantAttrs, roSpan.Attributes(), "exected attributes")
|
||||
assert.Equal(t, test.wantDropped, roSpan.DroppedAttributes(), "dropped attributes")
|
||||
|
||||
snap, ok := te.GetSpan(spanName)
|
||||
require.Truef(t, ok, "span %s not exported", spanName)
|
||||
|
||||
// Ensure the exported span snapshot is valid.
|
||||
assert.ElementsMatch(t, test.wantAttrs, snap.Attributes(), "exected attributes")
|
||||
assert.Equal(t, test.wantDropped, snap.DroppedAttributes(), "dropped attributes")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1852,3 +1945,11 @@ func TestWithIDGenerator(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyRecordingSpanAttributes(t *testing.T) {
|
||||
assert.Nil(t, (&recordingSpan{}).Attributes())
|
||||
}
|
||||
|
||||
func TestEmptyRecordingSpanDroppedAttributes(t *testing.T) {
|
||||
assert.Equal(t, 0, (&recordingSpan{}).DroppedAttributes())
|
||||
}
|
||||
|
||||
+8
-1
@@ -122,12 +122,19 @@ func (tr *tracer) newRecordingSpan(psc, sc trace.SpanContext, name string, sr Sa
|
||||
}
|
||||
|
||||
s := &recordingSpan{
|
||||
// Do not pre-allocate the attributes slice here! Doing so will
|
||||
// allocate memory that is likely never going to be used, or if used,
|
||||
// will be over-sized. The default Go compiler has been tested to
|
||||
// dynamically allocate needed space very well. Benchmarking has shown
|
||||
// it to be more performant than what we can predetermine here,
|
||||
// especially for the common use case of few to no added
|
||||
// attributes.
|
||||
|
||||
parent: psc,
|
||||
spanContext: sc,
|
||||
spanKind: trace.ValidateSpanKind(config.SpanKind()),
|
||||
name: name,
|
||||
startTime: startTime,
|
||||
attributes: newAttributesMap(tr.provider.spanLimits.AttributeCountLimit),
|
||||
events: newEvictedQueue(tr.provider.spanLimits.EventCountLimit),
|
||||
links: newEvictedQueue(tr.provider.spanLimits.LinkCountLimit),
|
||||
tracer: tr,
|
||||
|
||||
Reference in New Issue
Block a user