diff --git a/field.go b/field.go index f40fa7bf7..c8dd3358a 100644 --- a/field.go +++ b/field.go @@ -25,6 +25,7 @@ import ( "math" "time" + "go.uber.org/zap/internal/stacktrace" "go.uber.org/zap/zapcore" ) @@ -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 diff --git a/field_test.go b/field_test.go index bbb9f79f9..f87f1592e 100644 --- a/field_test.go +++ b/field_test.go @@ -29,6 +29,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "go.uber.org/zap/internal/stacktrace" "go.uber.org/zap/zapcore" ) @@ -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) } @@ -278,7 +279,7 @@ 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) } @@ -286,7 +287,7 @@ 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) } diff --git a/stacktrace.go b/internal/stacktrace/stack.go similarity index 75% rename from stacktrace.go rename to internal/stacktrace/stack.go index 1f152eb1a..82af7551f 100644 --- a/stacktrace.go +++ b/internal/stacktrace/stack.go @@ -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 @@ -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" @@ -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 @@ -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 } @@ -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) @@ -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. @@ -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') } diff --git a/stacktrace_test.go b/internal/stacktrace/stack_test.go similarity index 82% rename from stacktrace_test.go rename to internal/stacktrace/stack_test.go index 82b6af38e..195eeaeae 100644 --- a/stacktrace_test.go +++ b/internal/stacktrace/stack_test.go @@ -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 @@ -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" @@ -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( @@ -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.") @@ -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 { @@ -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) } } diff --git a/logger.go b/logger.go index 9b45b07b0..32d737276 100644 --- a/logger.go +++ b/logger.go @@ -27,6 +27,7 @@ import ( "strings" "go.uber.org/zap/internal/bufferpool" + "go.uber.org/zap/internal/stacktrace" "go.uber.org/zap/zapcore" ) @@ -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 { @@ -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.