Skip to content
This repository has been archived by the owner on Mar 6, 2023. It is now read-only.

Commit

Permalink
Add option for limiting the number of logs in a Span
Browse files Browse the repository at this point in the history
The MaxLogsPerSpan option limits the number of logs in a Span. Oldest events are
dropped as necessary. The first log in the finished span indicates how many were
dropped.

The default limit is 100.

Fixes #38.
  • Loading branch information
RaduBerinde committed Sep 26, 2016
1 parent c7c0202 commit ec9d41e
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 2 deletions.
40 changes: 39 additions & 1 deletion span.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package basictracer

import (
"fmt"
"sync"
"time"

Expand Down Expand Up @@ -28,6 +29,8 @@ type spanImpl struct {
event func(SpanEvent)
sync.Mutex // protects the fields below
raw RawSpan
// The number of logs dropped because of MaxLogsPerSpan.
numDroppedLogs int
}

var spanPool = &sync.Pool{New: func() interface{} {
Expand Down Expand Up @@ -113,7 +116,14 @@ func (s *spanImpl) Log(ld opentracing.LogData) {
ld.Timestamp = time.Now()
}

s.raw.Logs = append(s.raw.Logs, ld)
if maxLogs := s.tracer.options.MaxLogsPerSpan; maxLogs > 0 && len(s.raw.Logs) == maxLogs {
// We have too many logs. Overwrite the earliest log (use Logs as a circular
// buffer).
s.raw.Logs[s.numDroppedLogs%maxLogs] = ld
s.numDroppedLogs++
} else {
s.raw.Logs = append(s.raw.Logs, ld)
}
}

func (s *spanImpl) Finish() {
Expand All @@ -129,6 +139,34 @@ func (s *spanImpl) FinishWithOptions(opts opentracing.FinishOptions) {

s.Lock()
defer s.Unlock()

if s.numDroppedLogs > 0 {
// We dropped some log events, which means that we used Logs as a circular
// buffer. De-circularize it.
pos := s.numDroppedLogs % s.tracer.options.MaxLogsPerSpan
// The oldest event is on position pos. We rotate (circular shift) the
// buffer in place, moving the first pos elements to the back. This
// algorithm is described in:
// http://www.cplusplus.com/reference/algorithm/rotate
for first, middle, next := 0, pos, pos; first != middle; {
s.raw.Logs[first], s.raw.Logs[next] = s.raw.Logs[next], s.raw.Logs[first]
first++
next++
if next == len(s.raw.Logs) {
next = middle
} else if first == middle {
middle = next
}
}
// Replace the oldest event. This means we are effectively dropping one more
// event.
s.raw.Logs[0].Event = fmt.Sprintf("** dropped %d events **", s.numDroppedLogs+1)
s.raw.Logs[0].Payload = nil
// Let the timestamp be: this event will reflect the timestamp of the last
// dropped event.
}

// The MaxLogsPerSpan limit does not apply to BulkLogData.
if opts.BulkLogData != nil {
s.raw.Logs = append(s.raw.Logs, opts.BulkLogData...)
}
Expand Down
40 changes: 40 additions & 0 deletions span_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package basictracer

import (
"fmt"
"testing"

opentracing "github.com/opentracing/opentracing-go"
Expand Down Expand Up @@ -155,3 +156,42 @@ func TestSpan_DropAllLogs(t *testing.T) {
// Only logs are dropped
assert.Equal(t, 0, len(spans[0].Logs))
}

func TestSpan_MaxLogSperSpan(t *testing.T) {
for _, limit := range []int{1, 2, 3, 5, 10, 15, 20} {
for _, numLogs := range []int{1, 2, 3, 5, 10, 15, 20, 30, 40, 50} {
recorder := NewInMemoryRecorder()
// Tracer that only retains the last <limit> logs.
tracer := NewWithOptions(Options{
Recorder: recorder,
ShouldSample: func(traceID uint64) bool { return true }, // always sample
MaxLogsPerSpan: limit,
})

span := tracer.StartSpan("x")
for i := 0; i < numLogs; i++ {
span.LogEvent(fmt.Sprintf("event %d", i))
}
span.Finish()

spans := recorder.GetSpans()
assert.Equal(t, 1, len(spans))
assert.Equal(t, "x", spans[0].Operation)

logs := spans[0].Logs
if numLogs <= limit {
assert.Equal(t, numLogs, len(logs))
} else {
assert.Equal(t, limit, len(logs))
if len(logs) > 0 {
assert.Equal(t, fmt.Sprintf("** dropped %d events **", numLogs-limit+1), logs[0].Event)
logs = logs[1:]
}
}
// Verify that logs contains the last events.
for i, l := range logs {
assert.Equal(t, fmt.Sprintf("event %d", numLogs-len(logs)+i), l.Event)
}
}
}
}
10 changes: 9 additions & 1 deletion tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ type Options struct {
// DropAllLogs turns log events on all Spans into no-ops.
// If NewSpanEventListener is set, the callbacks will still fire.
DropAllLogs bool
// MaxLogsPerSpan limits the number of Logs in a span (if set to a nonzero
// value). If a span has more Logs than this value, the earlier Logs are
// dropped and replaced with a single log saying how many were dropped.
// The limit does not apply to any BulkLogData provided during Finish.
// This value is ignored if DropAllLogs is true. If NewSpanEventListener is
// set, the callbacks will still fire for all log events.
MaxLogsPerSpan int
// DebugAssertSingleGoroutine internally records the ID of the goroutine
// creating each Span and verifies that no operation is carried out on
// it on a different goroutine.
Expand Down Expand Up @@ -87,7 +94,8 @@ type Options struct {
// returned object with a Tracer.
func DefaultOptions() Options {
return Options{
ShouldSample: func(traceID uint64) bool { return traceID%64 == 0 },
ShouldSample: func(traceID uint64) bool { return traceID%64 == 0 },
MaxLogsPerSpan: 100,
}
}

Expand Down

0 comments on commit ec9d41e

Please sign in to comment.