Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add extended span to traceutils #28

Merged
merged 4 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 94 additions & 15 deletions util/traceutil/trace/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import (
"context"
"encoding/json"
"sync"
"time"

"github.com/eluv-io/common-go/format/duration"
"github.com/eluv-io/utc-go"

"github.com/eluv-io/common-go/format/duration"
)

// Span represents an operation that is named and timed. It may contain sub-spans, representing nested sub-operations. A
// span can also record an arbitrary number of attributes (key-value pairs) and events (bundled attributes with a name
// and timestamp)
type Span interface {
ExtendedSpan

// End completes the span. No updates are allowed to span after it
// ends. The only exception is setting status of the span.
End()
Expand Down Expand Up @@ -43,6 +47,23 @@ type Span interface {
FindByName(name string) Span
}

type ExtendedSpan interface {
// StartTime returns the start time of the span.
StartTime() utc.UTC

// EndTime returns the end time of the span.
EndTime() utc.UTC

// Duration returns the duration of the span.
Duration() time.Duration

// MarshalExtended returns true if the span is set to use an extended JSON representation during marshaling.
MarshalExtended() bool

// SetMarshalExtended sets the span to use an extended JSON representation during marshaling.
SetMarshalExtended()
}

type Event struct {
Name string `json:"name"`
Time utc.UTC `json:"-"`
Expand All @@ -65,33 +86,50 @@ func (n NoopSpan) Json() string { return
func (n NoopSpan) Attributes() map[string]interface{} { return nil }
func (n NoopSpan) Events() []*Event { return nil }
func (n NoopSpan) FindByName(string) Span { return nil }
func (n NoopSpan) StartTime() utc.UTC { return utc.Zero }
func (n NoopSpan) EndTime() utc.UTC { return utc.Zero }
func (n NoopSpan) Duration() time.Duration { return 0 }
func (n NoopSpan) MarshalExtended() bool { return false }
func (n NoopSpan) SetMarshalExtended() {}

// ---------------------------------------------------------------------------------------------------------------------

func newSpan(name string) *RecordingSpan {
s := &RecordingSpan{
StartTime: utc.Now(),
startTime: utc.Now(),
}
s.Data.Name = name
s.Data.Start = s.startTime.String()
return s
}

type RecordingSpan struct {
mutex sync.Mutex
Parent Span
StartTime utc.UTC
EndTime utc.UTC
Data struct {
Name string `json:"name"`
Duration duration.Spec `json:"time"`
Attr map[string]interface{} `json:"attr,omitempty"`
Events []*Event `json:"evnt,omitempty"`
Subs []Span `json:"subs,omitempty"`
}
Data recordingExtendedData
startTime utc.UTC
endTime utc.UTC
duration time.Duration
extended bool
}

type recordingData struct {
Name string `json:"name"`
Duration duration.Spec `json:"time"`
Attr map[string]interface{} `json:"attr,omitempty"`
Events []*Event `json:"evnt,omitempty"`
Subs []Span `json:"subs,omitempty"`
}

type recordingExtendedData struct {
recordingData
Start string `json:"start"`
End string `json:"end"`
}

func (s *RecordingSpan) Start(ctx context.Context, name string) (context.Context, Span) {
sub := newSpan(name)
sub.Parent = s

s.mutex.Lock()
s.Data.Subs = append(s.Data.Subs, sub)
Expand All @@ -104,11 +142,13 @@ func (s *RecordingSpan) End() {
s.mutex.Lock()
defer s.mutex.Unlock()

if s.EndTime != utc.Zero {
if s.endTime != utc.Zero {
return
}
s.EndTime = utc.Now()
s.Data.Duration = duration.Spec(s.EndTime.Sub(s.StartTime)).RoundTo(1)
s.endTime = utc.Now()
s.duration = s.endTime.Sub(s.startTime)
s.Data.Duration = duration.Spec(s.duration).RoundTo(1)
s.Data.End = s.endTime.String()
}

func (s *RecordingSpan) Attribute(name string, val interface{}) {
Expand Down Expand Up @@ -174,6 +214,45 @@ func (s *RecordingSpan) FindByName(name string) Span {
return nil
}

func (s *RecordingSpan) StartTime() utc.UTC {
s.mutex.Lock()
defer s.mutex.Unlock()

return s.startTime
}

func (s *RecordingSpan) EndTime() utc.UTC {
s.mutex.Lock()
defer s.mutex.Unlock()

return s.endTime
}

func (s *RecordingSpan) Duration() time.Duration {
s.mutex.Lock()
defer s.mutex.Unlock()

return s.duration
}

func (s *RecordingSpan) MarshalExtended() bool {
if s.extended {
return true
} else if s.Parent != nil {
// Check parent for extended flag
return s.Parent.MarshalExtended()
}
return false
}

func (s *RecordingSpan) SetMarshalExtended() {
s.extended = true
}

func (s *RecordingSpan) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Data)
if s.MarshalExtended() {
return json.Marshal(s.Data)
} else {
return json.Marshal(s.Data.recordingData)
}
}
42 changes: 42 additions & 0 deletions util/traceutil/traceutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ package traceutil_test
import (
"context"
"io"
"regexp"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/eluv-io/utc-go"

"github.com/eluv-io/common-go/util/traceutil"
"github.com/eluv-io/common-go/util/traceutil/trace"
)

const spanJson = `{"name":"root-span","time":"1s","subs":[{"name":"sub-span","time":"1s"}]}`

const spanExtendedJson = `{"name":"root-span","time":"1s","subs":[{"name":"sub-span","time":"1s","start":"2020-02-02T00:00:00","end":"2020-02-02T00:00:01"}],"start":"2020-02-02T00:00:00","end":"2020-02-02T00:00:01"}`

func TestStartSubSpan(t *testing.T) {
ctx, span := trace.StartRootSpan(context.Background(), "root-span")
require.NotNil(t, ctx)
Expand Down Expand Up @@ -58,3 +66,37 @@ func TestWithSubSpan(t *testing.T) {
require.Equal(t, "sub-a", subA.Data.Name)
require.Equal(t, "sub-b", subB.Data.Name)
}

func TestExtendedSpan(t *testing.T) {
var now time.Time
defer utc.MockNowFn(func() utc.UTC {
return utc.MustParse("2020-02-02").Add(time.Since(now))
})()

now = time.Now()
ctx, span := trace.StartRootSpan(context.Background(), "root-span")
require.NotNil(t, ctx)
require.NotNil(t, span)

ctx, sub := traceutil.StartSubSpan(ctx, "sub-span")
require.NotNil(t, ctx)
require.NotNil(t, sub)

time.Sleep(time.Second)

sub.End()
span.End()

require.False(t, span.MarshalExtended())
require.False(t, sub.MarshalExtended())
require.Equal(t, spanJson, span.Json())

span.SetMarshalExtended()
require.True(t, span.MarshalExtended())
require.True(t, sub.MarshalExtended())
require.Equal(t, spanExtendedJson, removeMs(span.Json()))
}

func removeMs(s string) string {
return regexp.MustCompile(`\.00\dZ`).ReplaceAllString(s, "")
}
Loading