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

internal: stacktrace: skip internal packages #2697

Merged
merged 4 commits into from
May 21, 2024
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/confluentinc/confluent-kafka-go/v2 v2.2.0
github.com/denisenkom/go-mssqldb v0.11.0
github.com/dimfeld/httptreemux/v5 v5.5.0
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4
github.com/elastic/go-elasticsearch/v6 v6.8.5
github.com/elastic/go-elasticsearch/v7 v7.17.1
github.com/elastic/go-elasticsearch/v8 v8.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
eliottness marked this conversation as resolved.
Show resolved Hide resolved
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg=
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds=
github.com/ebitengine/purego v0.6.0-alpha.5 h1:EYID3JOAdmQ4SNZYJHu9V6IqOeRQDBYxqKAg9PyoHFY=
github.com/ebitengine/purego v0.6.0-alpha.5/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE=
Expand Down
41 changes: 27 additions & 14 deletions internal/stacktrace/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal"

"github.com/google/uuid"
"github.com/tinylib/msgp/msgp"
)

Expand Down Expand Up @@ -45,28 +44,42 @@ type Event struct {
}

// NewEvent creates a new stacktrace event with the given category, type and message
func NewEvent(eventCat EventCategory, eventType, message string) *Event {
return &Event{
func NewEvent(eventCat EventCategory, options ...Options) *Event {
event := &Event{
Category: eventCat,
Type: eventType,
Language: "go",
Message: message,
Frames: SkipAndCapture(defaultCallerSkip),
}

for _, opt := range options {
opt(event)
}

return event
}

// IDLink returns a UUID to link the stacktrace event with other data. NOT thread-safe
func (e *Event) IDLink() string {
if e.ID != "" {
newUUID, err := uuid.NewUUID()
if err != nil {
return ""
}
// Options is a function type to set optional parameters for the event
type Options func(*Event)

// WithType sets the type of the event
func WithType(eventType string) Options {
return func(event *Event) {
event.Type = eventType
}
}

e.ID = newUUID.String()
// WithMessage sets the message of the event
func WithMessage(message string) Options {
return func(event *Event) {
event.Message = message
}
}

return e.ID
// WithID sets the id of the event
func WithID(id string) Options {
return func(event *Event) {
event.ID = id
}
}

// AddToSpan adds the event to the given span's root span as a tag if stacktrace collection is enabled
Expand Down
9 changes: 5 additions & 4 deletions internal/stacktrace/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@ import (
)

func TestNewEvent(t *testing.T) {
event := NewEvent(ExceptionEvent, "", "message")
event := NewEvent(ExceptionEvent, WithMessage("message"), WithType("type"), WithID("id"))
require.Equal(t, ExceptionEvent, event.Category)
require.Equal(t, "go", event.Language)
require.Equal(t, "message", event.Message)
require.GreaterOrEqual(t, len(event.Frames), 3)
require.Equal(t, "TestNewEvent", event.Frames[0].Function)
require.Equal(t, "type", event.Type)
require.Equal(t, "id", event.ID)
require.GreaterOrEqual(t, len(event.Frames), 2)
}

func TestEventToSpan(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

span := ddtracer.StartSpan("op")
event := NewEvent(ExceptionEvent, "", "message")
event := NewEvent(ExceptionEvent, WithMessage("message"))
AddToSpan(span, event)
span.Finish()

Expand Down
169 changes: 108 additions & 61 deletions internal/stacktrace/stacktrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,28 @@ import (
"os"
"regexp"
"runtime"
"strings"

"gopkg.in/DataDog/dd-trace-go.v1/internal/log"

"github.com/eapache/queue/v2"
"github.com/hashicorp/go-secure-stdlib/parseutil"
)

var (
enabled = true
defaultTopFrameDepth = 8
defaultMaxDepth = 32

// internalPackagesPrefixes is the list of prefixes for internal packages that should be hidden in the stack trace
internalSymbolPrefixes = []string{
"gopkg.in/DataDog/dd-trace-go.v1",
"github.com/DataDog/dd-trace-go",
"github.com/DataDog/go-libddwaf",
"github.com/DataDog/datadog-agent",
"github.com/DataDog/appsec-internal-go",
"github.com/DataDog/orchestrion",
}
)

const (
Expand Down Expand Up @@ -116,86 +128,121 @@ func Capture() StackTrace {

// SkipAndCapture creates a new stack trace from the current call stack, skipping the first `skip` frames
func SkipAndCapture(skip int) StackTrace {
// callers() and getRealStackDepth() have to be used side by side to keep the same number of `skip`-ed frames
realDepth := getRealStackDepth(skip, defaultMaxDepth)
callers := callers(skip, realDepth, defaultMaxDepth, defaultTopFrameDepth)
frames := callersFrame(callers, defaultMaxDepth, defaultTopFrameDepth)
stack := make([]StackFrame, len(frames))

for i := 0; i < len(frames); i++ {
frame := frames[i]

// If the top frames are separated from the bottom frames we have to stitch the real index together
frameIndex := i
if frameIndex >= defaultMaxDepth-defaultTopFrameDepth {
frameIndex = realDepth - defaultMaxDepth + i
return skipAndCapture(skip, defaultMaxDepth, internalSymbolPrefixes)
}

func skipAndCapture(skip int, maxDepth int, symbolSkip []string) StackTrace {
iter := iterator(skip, maxDepth, symbolSkip)
stack := make([]StackFrame, defaultMaxDepth)
nbStoredFrames := 0
topFramesQueue := queue.New[StackFrame]()

// We have to make sure we don't store more than maxDepth frames
// if there is more than maxDepth frames, we get X frames from the bottom of the stack and Y from the top
for frame, ok := iter.Next(); ok; frame, ok = iter.Next() {
// we reach the top frames: start to use the queue
if nbStoredFrames >= defaultMaxDepth-defaultTopFrameDepth {
topFramesQueue.Add(frame)
// queue is full, remove the oldest frame
if topFramesQueue.Length() > defaultTopFrameDepth {
topFramesQueue.Remove()
}
continue
}

parsedSymbol := parseSymbol(frame.Function)
// Bottom frames: directly store them in the stack
stack[nbStoredFrames] = frame
nbStoredFrames++
}

stack[i] = StackFrame{
Index: uint32(frameIndex),
Text: "",
File: frame.File,
Line: uint32(frame.Line),
Column: 0, // No column given by the runtime
Namespace: parsedSymbol.Package,
ClassName: parsedSymbol.Receiver,
Function: parsedSymbol.Function,
}
// Stitch the top frames to the stack
for topFramesQueue.Length() > 0 {
stack[nbStoredFrames] = topFramesQueue.Remove()
nbStoredFrames++
}

return stack
return stack[:nbStoredFrames]
}

// getRealStackDepth returns the real depth of the stack, skipping the first `skip` frames
func getRealStackDepth(skip, increment int) int {
pcs := make([]uintptr, increment)
// framesIterator is an iterator over the frames of a call stack
// It skips internal packages and caches the frames to avoid multiple calls to runtime.Callers
// It also skips the first `skip` frames
// It is not thread-safe
type framesIterator struct {
skipPrefixes []string
cache []uintptr
frames *queue.Queue[runtime.Frame]
cacheDepth int
cacheSize int
currDepth int
}

depth := increment
for n := increment; n == increment; depth += n {
n = runtime.Callers(depth+skip, pcs[:])
func iterator(skip, cacheSize int, internalPrefixSkip []string) framesIterator {
return framesIterator{
skipPrefixes: internalPrefixSkip,
cache: make([]uintptr, cacheSize),
frames: queue.New[runtime.Frame](),
cacheDepth: skip,
cacheSize: cacheSize,
currDepth: 0,
}

return depth
}

// callers returns an array of function pointers of size stackSize, skipping the first `skip` frames
// if realDepth of the current call stack is bigger that stackSize, we return the first stackSize - defaultTopFrameDepth frames
// and the last defaultTopFrameDepth frames of the whole stack
func callers(skip, realDepth, stackSize, topFrames int) []uintptr {
// The stack size is smaller than the max depth, return the whole stack
if realDepth <= stackSize {
pcs := make([]uintptr, realDepth)
runtime.Callers(skip, pcs[:])
return pcs
// next returns the next runtime.Frame in the call stack, filling the cache if needed
func (it *framesIterator) next() (runtime.Frame, bool) {
if it.frames.Length() == 0 {
n := runtime.Callers(it.cacheDepth, it.cache)
if n == 0 {
return runtime.Frame{}, false
}

frames := runtime.CallersFrames(it.cache[:n])
for {
frame, more := frames.Next()
it.frames.Add(frame)
it.cacheDepth++
if !more {
break
}
}
}

// The stack is bigger than the max depth, proceed to find the N start frames and stitch them to the ones we have
pcs := make([]uintptr, stackSize)
runtime.Callers(skip, pcs[:stackSize-topFrames])
runtime.Callers(skip+realDepth-topFrames, pcs[stackSize-topFrames:])
return pcs
it.currDepth++
return it.frames.Remove(), true
}

// callersFrame returns an array of runtime.Frame from an array of function pointers
// There can be multiple frames for a single function pointer, so we have to cut things again to make sure the final
// stacktrace is the correct size
func callersFrame(pcs []uintptr, stackSize, topFrames int) []runtime.Frame {
frames := runtime.CallersFrames(pcs)
framesArray := make([]runtime.Frame, 0, len(pcs))

// Next returns the next StackFrame in the call stack, skipping internal packages and refurbishing the cache if needed
func (it *framesIterator) Next() (StackFrame, bool) {
for {
frame, more := frames.Next()
framesArray = append(framesArray, frame)
if !more {
break
frame, ok := it.next()
if !ok {
return StackFrame{}, false
}

if it.skipSymbol(frame.Function) {
continue
}

parsedSymbol := parseSymbol(frame.Function)
return StackFrame{
Index: uint32(it.currDepth - 1),
Text: "",
File: frame.File,
Line: uint32(frame.Line),
Column: 0, // No column given by the runtime
Namespace: parsedSymbol.Package,
ClassName: parsedSymbol.Receiver,
Function: parsedSymbol.Function,
}, true
}
}

if len(framesArray) > stackSize {
framesArray = append(framesArray[:stackSize-topFrames], framesArray[len(framesArray)-topFrames:]...)
func (it *framesIterator) skipSymbol(symbol string) bool {
for _, prefix := range it.skipPrefixes {
if strings.HasPrefix(symbol, prefix) {
return true
}
}

return framesArray
return false
}
Loading
Loading