Skip to content
Closed
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go.work*
# profiling
*.test
*.out
*.prof

# generic
.DS_Store
Expand Down
26 changes: 26 additions & 0 deletions internal/stacktrace/parse_symbol_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.

package stacktrace

import "testing"

func BenchmarkParseSymbol(b *testing.B) {
testCases := []string{
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc",
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException",
"github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc.func1",
"os/exec.(*Cmd).Run.func1",
"test.(*Test).Method",
"test.main",
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range testCases {
_ = parseSymbol(tc)
}
}
}
105 changes: 83 additions & 22 deletions internal/stacktrace/stacktrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package stacktrace

import (
"errors"
"regexp"
"runtime"
"slices"
"strconv"
Expand Down Expand Up @@ -179,43 +178,105 @@ func (q *queue[T]) Remove() T {
return item
}

var symbolRegex = regexp.MustCompile(`^(([^(]+/)?([^(/.]+)?)(\.\(([^/)]+)\))?\.([^/()]+)$`)

// parseSymbol parses a symbol name into its package, receiver and function
// ex: github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
// -> receiver: *Event
// -> function: NewException
// parseSymbol parses a symbol name into its package, receiver and function using
// zero-allocation string operations. This is a hot path called once per stack frame.
//
// Handles various Go symbol formats:
// - Simple function: pkg.Function
// - Method with receiver: pkg.(*Type).Method or pkg.(Type).Method
// - Lambda/closure: pkg.Function.func1 or pkg.(*Type).Method.func1
// - Generics: pkg.(*Type[...]).Method or pkg.Function[...]
//
// Examples:
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.(*Event).NewException
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
// -> receiver: *Event
// -> function: NewException
// github.com/DataDog/dd-trace-go/v2/internal/stacktrace.TestFunc.func1
// -> package: github.com/DataDog/dd-trace-go/v2/internal/stacktrace
// -> receiver: ""
// -> function: TestFunc.func1
func parseSymbol(name string) symbol {
matches := symbolRegex.FindStringSubmatch(name)
if len(matches) != 7 {
log.Error("Failed to parse symbol for stacktrace: %s", name)
return symbol{
Package: "",
Receiver: "",
Function: "",
// Check for receiver first: pkg.(*Type) or pkg.(Type)
// Look for ".(" which marks the start of a receiver
if idx := strings.Index(name, ".("); idx != -1 {
// Find the closing paren of the receiver
receiverEnd := strings.IndexByte(name[idx+2:], ')')
if receiverEnd != -1 {
pkg := name[:idx]
receiver := name[idx+2 : idx+2+receiverEnd]
// Everything after ")." is the function (which may contain dots for lambdas)
fn := name[idx+2+receiverEnd+2:] // +2 for ")."
return symbol{
Package: pkg,
Receiver: receiver,
Function: fn,
}
}
}

// No receiver case: need to find where package ends and function begins
// Package path ends at the last '/' followed by a segment before first '.'
// Examples:
// "pkg.Function" -> pkg: "pkg", fn: "Function"
// "pkg.Function.func1" -> pkg: "pkg", fn: "Function.func1"
// "github.com/org/pkg.Function" -> pkg: "github.com/org/pkg", fn: "Function"

// Find the last slash to identify where the package name starts
lastSlash := strings.LastIndexByte(name, '/')

// Find the first dot after the last slash (or from the beginning if no slash)
searchStart := 0
if lastSlash != -1 {
searchStart = lastSlash + 1
}

firstDotAfterSlash := strings.IndexByte(name[searchStart:], '.')
if firstDotAfterSlash == -1 {
// No dots after last slash, the whole thing is the function name
return symbol{Function: name}
}

// Package ends at this dot, function starts after it
pkgEnd := searchStart + firstDotAfterSlash
return symbol{
Package: matches[1],
Receiver: matches[5],
Function: matches[6],
Package: name[:pkgEnd],
Function: name[pkgEnd+1:], // Everything after the dot, including nested dots for lambdas
}
}

// captureStack is the core stack capture implementation used by all public capture functions.
// It captures a stack trace with the given skip count, depth limit, and frame processing options.
func captureStack(skip int, maxDepth int, opts frameOptions) StackTrace {
return iterator(skip, maxDepth, opts).capture()
}

// Capture create a new stack trace from the current call stack
func Capture() StackTrace {
return SkipAndCapture(defaultCallerSkip)
}

// SkipAndCapture creates a new stack trace from the current call stack, skipping the first `skip` frames
func SkipAndCapture(skip int) StackTrace {
return iterator(skip, defaultMaxDepth, frameOptions{
return captureStack(skip, defaultMaxDepth, frameOptions{
skipInternalFrames: true,
redactCustomerFrames: false,
internalPackagePrefixes: internalSymbolPrefixes,
}).capture()
})
}

// SkipAndCaptureWithInternalFrames creates a new stack trace from the current call stack without filtering internal frames.
// This is useful for tracer span error stacktraces where we want to capture all frames.
func SkipAndCaptureWithInternalFrames(depth int, skip int) StackTrace {
// Use default depth if not specified
if depth == 0 {
depth = defaultMaxDepth
}
return captureStack(skip, depth, frameOptions{
skipInternalFrames: false,
redactCustomerFrames: false,
internalPackagePrefixes: nil,
})
}

// CaptureRaw captures only program counters without symbolication.
Expand All @@ -234,11 +295,11 @@ func CaptureRaw(skip int) RawStackTrace {
// This is designed for telemetry logging where we want to see internal frames for debugging
// but need to redact customer code for security
func CaptureWithRedaction(skip int) StackTrace {
return iterator(skip+1, defaultMaxDepth, frameOptions{
return captureStack(skip+1, defaultMaxDepth, frameOptions{
skipInternalFrames: false, // Keep DD internal frames
redactCustomerFrames: true, // Redact customer code
internalPackagePrefixes: internalSymbolPrefixes,
}).capture()
})
}

// Symbolicate converts raw PCs to a full StackTrace with symbolication,
Expand Down
Loading
Loading