Skip to content

Commit 5bfa9c5

Browse files
authored
Log warning when a trace attribute/event/link is discarded due to limits (open-telemetry#5434)
Fix open-telemetry#5343 - Update the `evictionQueue` to log when it drops a value - Update the `evictionQueue` to be declared over an `[T any]` parameter so it knows what to log when it is dropping a value and to reduce the `interface{}` allocation - Add a `clone` method to replace the now unneeded `interfaceArrayTo*Array` functions. - Update the `recordingSpan` to log once that is dropped an attribute when limits are reached.
1 parent fad23ee commit 5bfa9c5

File tree

6 files changed

+95
-42
lines changed

6 files changed

+95
-42
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1818
- Log a warning to the OpenTelemetry internal logger when a `Record` in `go.opentelemetry.io/otel/sdk/log` drops an attribute due to a limit being reached. (#5376)
1919
- Identify the `Tracer` returned from the global `TracerProvider` in `go.opentelemetry.io/otel/global` with its schema URL. (#5426)
2020
- Identify the `Meter` returned from the global `MeterProvider` in `go.opentelemetry.io/otel/global` with its schema URL. (#5426)
21+
- Log a warning to the OpenTelemetry internal logger when a `Span` in `go.opentelemetry.io/otel/sdk/trace` drops an attribute, event, or link due to a limit being reached. (#5434)
2122

2223
## [1.27.0/0.49.0/0.3.0] 2024-05-21
2324

sdk/trace/evictedqueue.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,38 @@
33

44
package trace // import "go.opentelemetry.io/otel/sdk/trace"
55

6+
import (
7+
"fmt"
8+
"slices"
9+
"sync"
10+
11+
"go.opentelemetry.io/otel/internal/global"
12+
)
13+
614
// evictedQueue is a FIFO queue with a configurable capacity.
7-
type evictedQueue struct {
8-
queue []interface{}
15+
type evictedQueue[T any] struct {
16+
queue []T
917
capacity int
1018
droppedCount int
19+
logDropped func()
1120
}
1221

13-
func newEvictedQueue(capacity int) evictedQueue {
22+
func newEvictedQueue[T any](capacity int) evictedQueue[T] {
23+
var tVal T
24+
msg := fmt.Sprintf("limit reached: dropping trace %T", tVal)
1425
// Do not pre-allocate queue, do this lazily.
15-
return evictedQueue{capacity: capacity}
26+
return evictedQueue[T]{
27+
capacity: capacity,
28+
logDropped: sync.OnceFunc(func() { global.Warn(msg) }),
29+
}
1630
}
1731

1832
// add adds value to the evictedQueue eq. If eq is at capacity, the oldest
1933
// queued value will be discarded and the drop count incremented.
20-
func (eq *evictedQueue) add(value interface{}) {
34+
func (eq *evictedQueue[T]) add(value T) {
2135
if eq.capacity == 0 {
2236
eq.droppedCount++
37+
eq.logDropped()
2338
return
2439
}
2540

@@ -28,6 +43,12 @@ func (eq *evictedQueue) add(value interface{}) {
2843
copy(eq.queue[:eq.capacity-1], eq.queue[1:])
2944
eq.queue = eq.queue[:eq.capacity-1]
3045
eq.droppedCount++
46+
eq.logDropped()
3147
}
3248
eq.queue = append(eq.queue, value)
3349
}
50+
51+
// copy returns a copy of the evictedQueue.
52+
func (eq *evictedQueue[T]) copy() []T {
53+
return slices.Clone(eq.queue)
54+
}

sdk/trace/evictedqueue_test.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,47 @@ package trace
66
import (
77
"reflect"
88
"testing"
9+
10+
"github.com/stretchr/testify/assert"
911
)
1012

1113
func init() {
1214
}
1315

1416
func TestAdd(t *testing.T) {
15-
q := newEvictedQueue(3)
17+
q := newEvictedQueue[string](3)
1618
q.add("value1")
1719
q.add("value2")
1820
if wantLen, gotLen := 2, len(q.queue); wantLen != gotLen {
1921
t.Errorf("got queue length %d want %d", gotLen, wantLen)
2022
}
2123
}
2224

23-
func (eq *evictedQueue) queueToArray() []string {
24-
arr := make([]string, 0)
25-
for _, value := range eq.queue {
26-
arr = append(arr, value.(string))
27-
}
28-
return arr
25+
func TestCopy(t *testing.T) {
26+
q := newEvictedQueue[string](3)
27+
q.add("value1")
28+
cp := q.copy()
29+
30+
q.add("value2")
31+
assert.Equal(t, []string{"value1"}, cp, "queue update modified copy")
32+
33+
cp[0] = "value0"
34+
assert.Equal(t, "value1", q.queue[0], "copy update modified queue")
2935
}
3036

3137
func TestDropCount(t *testing.T) {
32-
q := newEvictedQueue(3)
38+
q := newEvictedQueue[string](3)
39+
var called bool
40+
q.logDropped = func() { called = true }
41+
3342
q.add("value1")
43+
assert.False(t, called, `"value1" logged as dropped`)
3444
q.add("value2")
45+
assert.False(t, called, `"value2" logged as dropped`)
3546
q.add("value3")
47+
assert.False(t, called, `"value3" logged as dropped`)
3648
q.add("value1")
49+
assert.True(t, called, `"value2" not logged as dropped`)
3750
q.add("value4")
3851
if wantLen, gotLen := 3, len(q.queue); wantLen != gotLen {
3952
t.Errorf("got queue length %d want %d", gotLen, wantLen)
@@ -42,7 +55,7 @@ func TestDropCount(t *testing.T) {
4255
t.Errorf("got drop count %d want %d", gotDropCount, wantDropCount)
4356
}
4457
wantArr := []string{"value3", "value1", "value4"}
45-
gotArr := q.queueToArray()
58+
gotArr := q.copy()
4659

4760
if wantLen, gotLen := len(wantArr), len(gotArr); gotLen != wantLen {
4861
t.Errorf("got array len %d want %d", gotLen, wantLen)

sdk/trace/span.go

+28-26
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"go.opentelemetry.io/otel/attribute"
1919
"go.opentelemetry.io/otel/codes"
20+
"go.opentelemetry.io/otel/internal/global"
2021
"go.opentelemetry.io/otel/sdk/instrumentation"
2122
"go.opentelemetry.io/otel/sdk/internal"
2223
"go.opentelemetry.io/otel/sdk/resource"
@@ -137,12 +138,13 @@ type recordingSpan struct {
137138
// ReadOnlySpan exported when the span ends.
138139
attributes []attribute.KeyValue
139140
droppedAttributes int
141+
logDropAttrsOnce sync.Once
140142

141143
// events are stored in FIFO queue capped by configured limit.
142-
events evictedQueue
144+
events evictedQueue[Event]
143145

144146
// links are stored in FIFO queue capped by configured limit.
145-
links evictedQueue
147+
links evictedQueue[Link]
146148

147149
// executionTracerTaskEnd ends the execution tracer span.
148150
executionTracerTaskEnd func()
@@ -219,7 +221,7 @@ func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) {
219221
limit := s.tracer.provider.spanLimits.AttributeCountLimit
220222
if limit == 0 {
221223
// No attributes allowed.
222-
s.droppedAttributes += len(attributes)
224+
s.addDroppedAttr(len(attributes))
223225
return
224226
}
225227

@@ -236,14 +238,30 @@ func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) {
236238
for _, a := range attributes {
237239
if !a.Valid() {
238240
// Drop all invalid attributes.
239-
s.droppedAttributes++
241+
s.addDroppedAttr(1)
240242
continue
241243
}
242244
a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
243245
s.attributes = append(s.attributes, a)
244246
}
245247
}
246248

249+
// Declared as a var so tests can override.
250+
var logDropAttrs = func() {
251+
global.Warn("limit reached: dropping trace Span attributes")
252+
}
253+
254+
// addDroppedAttr adds incr to the count of dropped attributes.
255+
//
256+
// The first, and only the first, time this method is called a warning will be
257+
// logged.
258+
//
259+
// This method assumes s.mu.Lock is held by the caller.
260+
func (s *recordingSpan) addDroppedAttr(incr int) {
261+
s.droppedAttributes += incr
262+
s.logDropAttrsOnce.Do(logDropAttrs)
263+
}
264+
247265
// addOverCapAttrs adds the attributes attrs to the span s while
248266
// de-duplicating the attributes of s and attrs and dropping attributes that
249267
// exceed the limit.
@@ -273,7 +291,7 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
273291
for _, a := range attrs {
274292
if !a.Valid() {
275293
// Drop all invalid attributes.
276-
s.droppedAttributes++
294+
s.addDroppedAttr(1)
277295
continue
278296
}
279297

@@ -286,7 +304,7 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
286304
if len(s.attributes) >= limit {
287305
// Do not just drop all of the remaining attributes, make sure
288306
// updates are checked and performed.
289-
s.droppedAttributes++
307+
s.addDroppedAttr(1)
290308
} else {
291309
a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
292310
s.attributes = append(s.attributes, a)
@@ -585,7 +603,7 @@ func (s *recordingSpan) Links() []Link {
585603
if len(s.links.queue) == 0 {
586604
return []Link{}
587605
}
588-
return s.interfaceArrayToLinksArray()
606+
return s.links.copy()
589607
}
590608

591609
// Events returns the events of this span.
@@ -595,7 +613,7 @@ func (s *recordingSpan) Events() []Event {
595613
if len(s.events.queue) == 0 {
596614
return []Event{}
597615
}
598-
return s.interfaceArrayToEventArray()
616+
return s.events.copy()
599617
}
600618

601619
// Status returns the status of this span.
@@ -717,32 +735,16 @@ func (s *recordingSpan) snapshot() ReadOnlySpan {
717735
}
718736
sd.droppedAttributeCount = s.droppedAttributes
719737
if len(s.events.queue) > 0 {
720-
sd.events = s.interfaceArrayToEventArray()
738+
sd.events = s.events.copy()
721739
sd.droppedEventCount = s.events.droppedCount
722740
}
723741
if len(s.links.queue) > 0 {
724-
sd.links = s.interfaceArrayToLinksArray()
742+
sd.links = s.links.copy()
725743
sd.droppedLinkCount = s.links.droppedCount
726744
}
727745
return &sd
728746
}
729747

730-
func (s *recordingSpan) interfaceArrayToLinksArray() []Link {
731-
linkArr := make([]Link, 0)
732-
for _, value := range s.links.queue {
733-
linkArr = append(linkArr, value.(Link))
734-
}
735-
return linkArr
736-
}
737-
738-
func (s *recordingSpan) interfaceArrayToEventArray() []Event {
739-
eventArr := make([]Event, 0)
740-
for _, value := range s.events.queue {
741-
eventArr = append(eventArr, value.(Event))
742-
}
743-
return eventArr
744-
}
745-
746748
func (s *recordingSpan) addChild() {
747749
if !s.IsRecording() {
748750
return

sdk/trace/span_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ func TestTruncateAttr(t *testing.T) {
234234
}
235235
}
236236

237+
func TestLogDropAttrs(t *testing.T) {
238+
orig := logDropAttrs
239+
t.Cleanup(func() { logDropAttrs = orig })
240+
241+
var called bool
242+
logDropAttrs = func() { called = true }
243+
244+
s := &recordingSpan{}
245+
s.addDroppedAttr(1)
246+
assert.True(t, called, "logDropAttrs not called")
247+
248+
called = false
249+
s.addDroppedAttr(1)
250+
assert.False(t, called, "logDropAttrs called multiple times for same Span")
251+
}
252+
237253
func BenchmarkRecordingSpanSetAttributes(b *testing.B) {
238254
var attrs []attribute.KeyValue
239255
for i := 0; i < 100; i++ {

sdk/trace/tracer.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ func (tr *tracer) newRecordingSpan(psc, sc trace.SpanContext, name string, sr Sa
132132
spanKind: trace.ValidateSpanKind(config.SpanKind()),
133133
name: name,
134134
startTime: startTime,
135-
events: newEvictedQueue(tr.provider.spanLimits.EventCountLimit),
136-
links: newEvictedQueue(tr.provider.spanLimits.LinkCountLimit),
135+
events: newEvictedQueue[Event](tr.provider.spanLimits.EventCountLimit),
136+
links: newEvictedQueue[Link](tr.provider.spanLimits.LinkCountLimit),
137137
tracer: tr,
138138
}
139139

0 commit comments

Comments
 (0)