Skip to content

Commit

Permalink
chore: Move stacktrace capturing into internal/ (#1341)
Browse files Browse the repository at this point in the history
Moves the functionality to capture and format stack traces
into an internal stacktrace package
and exports the relevant bits out for the logger and field.go to use.

This will be used by zapslog.Handler to capture stack traces
so it needs to be in a shared location.

Refs #1329, #1339
  • Loading branch information
abhinav authored Aug 25, 2023
1 parent 55a2367 commit 98e9c4f
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 54 deletions.
3 changes: 2 additions & 1 deletion field.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"math"
"time"

"go.uber.org/zap/internal/stacktrace"
"go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -374,7 +375,7 @@ func StackSkip(key string, skip int) Field {
// from expanding the zapcore.Field union struct to include a byte slice. Since
// taking a stacktrace is already so expensive (~10us), the extra allocation
// is okay.
return String(key, takeStacktrace(skip+1)) // skip StackSkip
return String(key, stacktrace.Take(skip+1)) // skip StackSkip
}

// Duration constructs a field with the given key and value. The encoder
Expand Down
7 changes: 4 additions & 3 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
"go.uber.org/zap/internal/stacktrace"
"go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -269,7 +270,7 @@ func TestStackField(t *testing.T) {
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
r := regexp.MustCompile(`field_test.go:(\d+)`)
assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), "Unexpected stack trace")
assert.Equal(t, r.ReplaceAllString(stacktrace.Take(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), "Unexpected stack trace")
assertCanBeReused(t, f)
}

Expand All @@ -278,15 +279,15 @@ func TestStackSkipField(t *testing.T) {
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
r := regexp.MustCompile(`field_test.go:(\d+)`)
assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), f.String, "Unexpected stack trace")
assert.Equal(t, r.ReplaceAllString(stacktrace.Take(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), f.String, "Unexpected stack trace")
assertCanBeReused(t, f)
}

func TestStackSkipFieldWithSkip(t *testing.T) {
f := StackSkip("stacktrace", 1)
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
assert.Equal(t, takeStacktrace(1), f.String, "Unexpected stack trace")
assert.Equal(t, stacktrace.Take(1), f.String, "Unexpected stack trace")
assertCanBeReused(t, f)
}

Expand Down
71 changes: 39 additions & 32 deletions stacktrace.go → internal/stacktrace/stack.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2016 Uber Technologies, Inc.
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,7 +18,9 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zap
// Package stacktrace provides support for gathering stack traces
// efficiently.
package stacktrace

import (
"runtime"
Expand All @@ -28,13 +30,14 @@ import (
"go.uber.org/zap/internal/pool"
)

var _stacktracePool = pool.New(func() *stacktrace {
return &stacktrace{
var _stackPool = pool.New(func() *Stack {
return &Stack{
storage: make([]uintptr, 64),
}
})

type stacktrace struct {
// Stack is a captured stack trace.
type Stack struct {
pcs []uintptr // program counters; always a subslice of storage
frames *runtime.Frames

Expand All @@ -48,30 +51,30 @@ type stacktrace struct {
storage []uintptr
}

// stacktraceDepth specifies how deep of a stack trace should be captured.
type stacktraceDepth int
// Depth specifies how deep of a stack trace should be captured.
type Depth int

const (
// stacktraceFirst captures only the first frame.
stacktraceFirst stacktraceDepth = iota
// First captures only the first frame.
First Depth = iota

// stacktraceFull captures the entire call stack, allocating more
// Full captures the entire call stack, allocating more
// storage for it if needed.
stacktraceFull
Full
)

// captureStacktrace captures a stack trace of the specified depth, skipping
// Capture captures a stack trace of the specified depth, skipping
// the provided number of frames. skip=0 identifies the caller of
// captureStacktrace.
// Capture.
//
// The caller must call Free on the returned stacktrace after using it.
func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace {
stack := _stacktracePool.Get()
func Capture(skip int, depth Depth) *Stack {
stack := _stackPool.Get()

switch depth {
case stacktraceFirst:
case First:
stack.pcs = stack.storage[:1]
case stacktraceFull:
case Full:
stack.pcs = stack.storage
}

Expand All @@ -85,7 +88,7 @@ func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace {
// runtime.Callers truncates the recorded stacktrace if there is no
// room in the provided slice. For the full stack trace, keep expanding
// storage until there are fewer frames than there is room.
if depth == stacktraceFull {
if depth == Full {
pcs := stack.pcs
for numFrames == len(pcs) {
pcs = make([]uintptr, len(pcs)*2)
Expand All @@ -107,50 +110,54 @@ func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace {

// Free releases resources associated with this stacktrace
// and returns it back to the pool.
func (st *stacktrace) Free() {
func (st *Stack) Free() {
st.frames = nil
st.pcs = nil
_stacktracePool.Put(st)
_stackPool.Put(st)
}

// Count reports the total number of frames in this stacktrace.
// Count DOES NOT change as Next is called.
func (st *stacktrace) Count() int {
func (st *Stack) Count() int {
return len(st.pcs)
}

// Next returns the next frame in the stack trace,
// and a boolean indicating whether there are more after it.
func (st *stacktrace) Next() (_ runtime.Frame, more bool) {
func (st *Stack) Next() (_ runtime.Frame, more bool) {
return st.frames.Next()
}

func takeStacktrace(skip int) string {
stack := captureStacktrace(skip+1, stacktraceFull)
// Take returns a string representation of the current stacktrace.
//
// skip is the number of frames to skip before recording the stack trace.
// skip=0 identifies the caller of Take.
func Take(skip int) string {
stack := Capture(skip+1, Full)
defer stack.Free()

buffer := bufferpool.Get()
defer buffer.Free()

stackfmt := newStackFormatter(buffer)
stackfmt := NewFormatter(buffer)
stackfmt.FormatStack(stack)
return buffer.String()
}

// stackFormatter formats a stack trace into a readable string representation.
type stackFormatter struct {
// Formatter formats a stack trace into a readable string representation.
type Formatter struct {
b *buffer.Buffer
nonEmpty bool // whehther we've written at least one frame already
}

// newStackFormatter builds a new stackFormatter.
func newStackFormatter(b *buffer.Buffer) stackFormatter {
return stackFormatter{b: b}
// NewFormatter builds a new Formatter.
func NewFormatter(b *buffer.Buffer) Formatter {
return Formatter{b: b}
}

// FormatStack formats all remaining frames in the provided stacktrace -- minus
// the final runtime.main/runtime.goexit frame.
func (sf *stackFormatter) FormatStack(stack *stacktrace) {
func (sf *Formatter) FormatStack(stack *Stack) {
// Note: On the last iteration, frames.Next() returns false, with a valid
// frame, but we ignore this frame. The last frame is a runtime frame which
// adds noise, since it's only either runtime.main or runtime.goexit.
Expand All @@ -160,7 +167,7 @@ func (sf *stackFormatter) FormatStack(stack *stacktrace) {
}

// FormatFrame formats the given frame.
func (sf *stackFormatter) FormatFrame(frame runtime.Frame) {
func (sf *Formatter) FormatFrame(frame runtime.Frame) {
if sf.nonEmpty {
sf.b.AppendByte('\n')
}
Expand Down
28 changes: 14 additions & 14 deletions stacktrace_test.go → internal/stacktrace/stack_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2016 Uber Technologies, Inc.
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zap
package stacktrace

import (
"bytes"
Expand All @@ -29,20 +29,20 @@ import (
"github.com/stretchr/testify/require"
)

func TestTakeStacktrace(t *testing.T) {
trace := takeStacktrace(0)
func TestTake(t *testing.T) {
trace := Take(0)
lines := strings.Split(trace, "\n")
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
assert.Contains(
t,
lines[0],
"go.uber.org/zap.TestTakeStacktrace",
"go.uber.org/zap/internal/stacktrace.TestTake",
"Expected stacktrace to start with the test.",
)
}

func TestTakeStacktraceWithSkip(t *testing.T) {
trace := takeStacktrace(1)
func TestTakeWithSkip(t *testing.T) {
trace := Take(1)
lines := strings.Split(trace, "\n")
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
assert.Contains(
Expand All @@ -53,10 +53,10 @@ func TestTakeStacktraceWithSkip(t *testing.T) {
)
}

func TestTakeStacktraceWithSkipInnerFunc(t *testing.T) {
func TestTakeWithSkipInnerFunc(t *testing.T) {
var trace string
func() {
trace = takeStacktrace(2)
trace = Take(2)
}()
lines := strings.Split(trace, "\n")
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
Expand All @@ -68,13 +68,13 @@ func TestTakeStacktraceWithSkipInnerFunc(t *testing.T) {
)
}

func TestTakeStacktraceDeepStack(t *testing.T) {
func TestTakeDeepStack(t *testing.T) {
const (
N = 500
withStackDepthName = "go.uber.org/zap.withStackDepth"
withStackDepthName = "go.uber.org/zap/internal/stacktrace.withStackDepth"
)
withStackDepth(N, func() {
trace := takeStacktrace(0)
trace := Take(0)
for found := 0; found < N; found++ {
i := strings.Index(trace, withStackDepthName)
if i < 0 {
Expand All @@ -86,9 +86,9 @@ func TestTakeStacktraceDeepStack(t *testing.T) {
})
}

func BenchmarkTakeStacktrace(b *testing.B) {
func BenchmarkTake(b *testing.B) {
for i := 0; i < b.N; i++ {
takeStacktrace(0)
Take(0)
}
}

Expand Down
9 changes: 5 additions & 4 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"

"go.uber.org/zap/internal/bufferpool"
"go.uber.org/zap/internal/stacktrace"
"go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -363,11 +364,11 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {

// Adding the caller or stack trace requires capturing the callers of
// this function. We'll share information between these two.
stackDepth := stacktraceFirst
stackDepth := stacktrace.First
if addStack {
stackDepth = stacktraceFull
stackDepth = stacktrace.Full
}
stack := captureStacktrace(log.callerSkip+callerSkipOffset, stackDepth)
stack := stacktrace.Capture(log.callerSkip+callerSkipOffset, stackDepth)
defer stack.Free()

if stack.Count() == 0 {
Expand All @@ -394,7 +395,7 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
buffer := bufferpool.Get()
defer buffer.Free()

stackfmt := newStackFormatter(buffer)
stackfmt := stacktrace.NewFormatter(buffer)

// We've already extracted the first frame, so format that
// separately and defer to stackfmt for the rest.
Expand Down

0 comments on commit 98e9c4f

Please sign in to comment.