Skip to content

Commit 2657839

Browse files
authored
Honor CallerSkip when taking stack traces & add the StackSkip field (#843)
* Honor `CallerSkip` when taking a stack trace. Both the caller and stack trace will now point to the same frame. * Add `StackSkip` which is similar to `Stack` but allows skipping frames from the top of the stack. This removes the internal behavior of skipping zap specific stack frames when taking a stack trace, and instead relies on the same behavior used to calculate the number of frames to skip for getting the caller to also skip frames in the stack trace. Fixes #512, fixes #727
1 parent 2314926 commit 2657839

File tree

7 files changed

+85
-78
lines changed

7 files changed

+85
-78
lines changed

config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestConfig(t *testing.T) {
5252
expectRe: "DEBUG\tzap/config_test.go:" + `\d+` + "\tdebug\t" + `{"k": "v", "z": "zz"}` + "\n" +
5353
"INFO\tzap/config_test.go:" + `\d+` + "\tinfo\t" + `{"k": "v", "z": "zz"}` + "\n" +
5454
"WARN\tzap/config_test.go:" + `\d+` + "\twarn\t" + `{"k": "v", "z": "zz"}` + "\n" +
55-
`testing.\w+`,
55+
`go.uber.org/zap.TestConfig.\w+`,
5656
},
5757
}
5858

field.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,17 @@ func Timep(key string, val *time.Time) Field {
364364
// expensive (relatively speaking); this function both makes an allocation and
365365
// takes about two microseconds.
366366
func Stack(key string) Field {
367+
return StackSkip(key, 1) // skip Stack
368+
}
369+
370+
// StackSkip constructs a field similarly to Stack, but also skips the given
371+
// number of frames from the top of the stacktrace.
372+
func StackSkip(key string, skip int) Field {
367373
// Returning the stacktrace as a string costs an allocation, but saves us
368374
// from expanding the zapcore.Field union struct to include a byte slice. Since
369375
// taking a stacktrace is already so expensive (~10us), the extra allocation
370376
// is okay.
371-
return String(key, takeStacktrace())
377+
return String(key, takeStacktrace(skip+1)) // skip StackSkip
372378
}
373379

374380
// Duration constructs a field with the given key and value. The encoder

field_test.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package zap
2323
import (
2424
"math"
2525
"net"
26+
"regexp"
2627
"sync"
2728
"testing"
2829
"time"
@@ -259,6 +260,24 @@ func TestStackField(t *testing.T) {
259260
f := Stack("stacktrace")
260261
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
261262
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
262-
assert.Equal(t, takeStacktrace(), f.String, "Unexpected stack trace")
263+
r := regexp.MustCompile(`field_test.go:(\d+)`)
264+
assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), "Unexpected stack trace")
265+
assertCanBeReused(t, f)
266+
}
267+
268+
func TestStackSkipField(t *testing.T) {
269+
f := StackSkip("stacktrace", 0)
270+
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
271+
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
272+
r := regexp.MustCompile(`field_test.go:(\d+)`)
273+
assert.Equal(t, r.ReplaceAllString(takeStacktrace(0), "field_test.go"), r.ReplaceAllString(f.String, "field_test.go"), f.String, "Unexpected stack trace")
274+
assertCanBeReused(t, f)
275+
}
276+
277+
func TestStackSkipFieldWithSkip(t *testing.T) {
278+
f := StackSkip("stacktrace", 1)
279+
assert.Equal(t, "stacktrace", f.Key, "Unexpected field key.")
280+
assert.Equal(t, zapcore.StringType, f.Type, "Unexpected field type.")
281+
assert.Equal(t, takeStacktrace(1), f.String, "Unexpected stack trace")
263282
assertCanBeReused(t, f)
264283
}

logger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
312312
}
313313
}
314314
if log.addStack.Enabled(ce.Entry.Level) {
315-
ce.Entry.Stack = Stack("").String
315+
ce.Entry.Stack = StackSkip("", log.callerSkip+callerSkipOffset).String
316316
}
317317

318318
return ce

stacktrace.go

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,30 @@ package zap
2222

2323
import (
2424
"runtime"
25-
"strings"
2625
"sync"
2726

2827
"go.uber.org/zap/internal/bufferpool"
2928
)
3029

31-
const _zapPackage = "go.uber.org/zap"
32-
3330
var (
3431
_stacktracePool = sync.Pool{
3532
New: func() interface{} {
3633
return newProgramCounters(64)
3734
},
3835
}
39-
40-
// We add "." and "/" suffixes to the package name to ensure we only match
41-
// the exact package and not any package with the same prefix.
42-
_zapStacktracePrefixes = addPrefix(_zapPackage, ".", "/")
43-
_zapStacktraceVendorContains = addPrefix("/vendor/", _zapStacktracePrefixes...)
4436
)
4537

46-
func takeStacktrace() string {
38+
func takeStacktrace(skip int) string {
4739
buffer := bufferpool.Get()
4840
defer buffer.Free()
4941
programCounters := _stacktracePool.Get().(*programCounters)
5042
defer _stacktracePool.Put(programCounters)
5143

5244
var numFrames int
5345
for {
54-
// Skip the call to runtime.Counters and takeStacktrace so that the
46+
// Skip the call to runtime.Callers and takeStacktrace so that the
5547
// program counters start at the caller of takeStacktrace.
56-
numFrames = runtime.Callers(2, programCounters.pcs)
48+
numFrames = runtime.Callers(skip+2, programCounters.pcs)
5749
if numFrames < len(programCounters.pcs) {
5850
break
5951
}
@@ -63,19 +55,12 @@ func takeStacktrace() string {
6355
}
6456

6557
i := 0
66-
skipZapFrames := true // skip all consecutive zap frames at the beginning.
6758
frames := runtime.CallersFrames(programCounters.pcs[:numFrames])
6859

6960
// Note: On the last iteration, frames.Next() returns false, with a valid
7061
// frame, but we ignore this frame. The last frame is a a runtime frame which
7162
// adds noise, since it's only either runtime.main or runtime.goexit.
7263
for frame, more := frames.Next(); more; frame, more = frames.Next() {
73-
if skipZapFrames && isZapFrame(frame.Function) {
74-
continue
75-
} else {
76-
skipZapFrames = false
77-
}
78-
7964
if i != 0 {
8065
buffer.AppendByte('\n')
8166
}
@@ -91,36 +76,10 @@ func takeStacktrace() string {
9176
return buffer.String()
9277
}
9378

94-
func isZapFrame(function string) bool {
95-
for _, prefix := range _zapStacktracePrefixes {
96-
if strings.HasPrefix(function, prefix) {
97-
return true
98-
}
99-
}
100-
101-
// We can't use a prefix match here since the location of the vendor
102-
// directory affects the prefix. Instead we do a contains match.
103-
for _, contains := range _zapStacktraceVendorContains {
104-
if strings.Contains(function, contains) {
105-
return true
106-
}
107-
}
108-
109-
return false
110-
}
111-
11279
type programCounters struct {
11380
pcs []uintptr
11481
}
11582

11683
func newProgramCounters(size int) *programCounters {
11784
return &programCounters{make([]uintptr, size)}
11885
}
119-
120-
func addPrefix(prefix string, ss ...string) []string {
121-
withPrefix := make([]string, len(ss))
122-
for i, s := range ss {
123-
withPrefix[i] = prefix + s
124-
}
125-
return withPrefix
126-
}

stacktrace_ext_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ func TestStacktraceFiltersVendorZap(t *testing.T) {
120120
})
121121
}
122122

123+
func TestStacktraceWithoutCallerSkip(t *testing.T) {
124+
withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
125+
func() {
126+
logger.Error("test log")
127+
}()
128+
129+
require.Contains(t, out.String(), "TestStacktraceWithoutCallerSkip.", "Should not skip too much")
130+
verifyNoZap(t, out.String())
131+
})
132+
}
133+
134+
func TestStacktraceWithCallerSkip(t *testing.T) {
135+
withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
136+
logger = logger.WithOptions(zap.AddCallerSkip(2))
137+
func() {
138+
logger.Error("test log")
139+
}()
140+
141+
require.NotContains(t, out.String(), "TestStacktraceWithCallerSkip.", "Should skip as requested by caller skip")
142+
require.Contains(t, out.String(), "TestStacktraceWithCallerSkip", "Should not skip too much")
143+
verifyNoZap(t, out.String())
144+
})
145+
}
146+
123147
// withLogger sets up a logger with a real encoder set up, so that any marshal functions are called.
124148
// The inbuilt observer does not call Marshal for objects/arrays, which we need for some tests.
125149
func withLogger(t *testing.T, fn func(logger *zap.Logger, out *bytes.Buffer)) {

stacktrace_test.go

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,47 +29,46 @@ import (
2929
)
3030

3131
func TestTakeStacktrace(t *testing.T) {
32-
trace := takeStacktrace()
32+
trace := takeStacktrace(0)
3333
lines := strings.Split(trace, "\n")
34-
require.True(t, len(lines) > 0, "Expected stacktrace to have at least one frame.")
34+
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
3535
assert.Contains(
3636
t,
3737
lines[0],
38-
"testing.",
39-
"Expected stacktrace to start with the test runner (zap frames are filtered out) %s.", lines[0],
38+
"go.uber.org/zap.TestTakeStacktrace",
39+
"Expected stacktrace to start with the test.",
4040
)
4141
}
4242

43-
func TestIsZapFrame(t *testing.T) {
44-
zapFrames := []string{
45-
"go.uber.org/zap.Stack",
46-
"go.uber.org/zap.(*SugaredLogger).log",
47-
"go.uber.org/zap/zapcore.(ArrayMarshalerFunc).MarshalLogArray",
48-
"github.com/uber/tchannel-go/vendor/go.uber.org/zap.Stack",
49-
"github.com/uber/tchannel-go/vendor/go.uber.org/zap.(*SugaredLogger).log",
50-
"github.com/uber/tchannel-go/vendor/go.uber.org/zap/zapcore.(ArrayMarshalerFunc).MarshalLogArray",
51-
}
52-
nonZapFrames := []string{
53-
"github.com/uber/tchannel-go.NewChannel",
54-
"go.uber.org/not-zap.New",
55-
"go.uber.org/zapext.ctx",
56-
"go.uber.org/zap_ext/ctx.New",
57-
}
43+
func TestTakeStacktraceWithSkip(t *testing.T) {
44+
trace := takeStacktrace(1)
45+
lines := strings.Split(trace, "\n")
46+
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
47+
assert.Contains(
48+
t,
49+
lines[0],
50+
"testing.",
51+
"Expected stacktrace to start with the test runner (skipping our own frame).",
52+
)
53+
}
5854

59-
t.Run("zap frames", func(t *testing.T) {
60-
for _, f := range zapFrames {
61-
require.True(t, isZapFrame(f), f)
62-
}
63-
})
64-
t.Run("non-zap frames", func(t *testing.T) {
65-
for _, f := range nonZapFrames {
66-
require.False(t, isZapFrame(f), f)
67-
}
68-
})
55+
func TestTakeStacktraceWithSkipInnerFunc(t *testing.T) {
56+
var trace string
57+
func() {
58+
trace = takeStacktrace(2)
59+
}()
60+
lines := strings.Split(trace, "\n")
61+
require.NotEmpty(t, lines, "Expected stacktrace to have at least one frame.")
62+
assert.Contains(
63+
t,
64+
lines[0],
65+
"testing.",
66+
"Expected stacktrace to start with the test function (skipping the test function).",
67+
)
6968
}
7069

7170
func BenchmarkTakeStacktrace(b *testing.B) {
7271
for i := 0; i < b.N; i++ {
73-
takeStacktrace()
72+
takeStacktrace(0)
7473
}
7574
}

0 commit comments

Comments
 (0)