Skip to content

Commit 6064240

Browse files
committed
Add a field factory for stacktraces (uber-go#20)
* Add a field factory for stacktraces Add a field factory that wraps `runtime.Stack`, so that users don't need to pick a byte size for their stacktraces. The wrapper attempts to re-use a buffer from the pool, but allocates if it's necessary to capture the whole stacktrace. Note that the Stack() function is *really* expensive - casting the trace to a string costs an allocation, but just taking a stacktrace is ~10 microseconds (roughly the same amount of time as logging 100 non-stacktrace fields). * Update from CR
1 parent 5e9183f commit 6064240

5 files changed

+118
-0
lines changed

field.go

+17
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,23 @@ func Err(err error) Field {
9292
return String("error", err.Error())
9393
}
9494

95+
// Stack constructs a Field that stores a stacktrace of the current goroutine
96+
// under the key "stacktrace". Keep in mind that taking a stacktrace is
97+
// extremely expensive (relatively speaking); this function both makes an
98+
// allocation and takes ~10 microseconds.
99+
func Stack() Field {
100+
// Try to avoid allocating a buffer.
101+
enc := newJSONEncoder()
102+
bs := enc.bytes[:cap(enc.bytes)]
103+
// Returning the stacktrace as a string costs an allocation, but saves us
104+
// from expanding the Field union struct to include a byte slice. Since
105+
// taking a stacktrace is already so expensive (~10us), the extra allocation
106+
// is okay.
107+
field := String("stacktrace", takeStacktrace(bs, false))
108+
enc.Free()
109+
return field
110+
}
111+
95112
// Duration constructs a Field with the given key and value. It represents
96113
// durations as an integer number of nanoseconds.
97114
func Duration(key string, val time.Duration) Field {

field_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ package zap
2222

2323
import (
2424
"errors"
25+
"strings"
2526
"sync"
2627
"testing"
2728
"time"
2829

2930
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
3032
)
3133

3234
type fakeUser struct{ name string }
@@ -129,3 +131,14 @@ func TestNestField(t *testing.T) {
129131
nest := Nest("foo", String("name", "phil"), Int("age", 42))
130132
assertCanBeReused(t, nest)
131133
}
134+
135+
func TestStackField(t *testing.T) {
136+
enc := newJSONEncoder()
137+
defer enc.Free()
138+
139+
Stack().addTo(enc)
140+
output := string(enc.bytes)
141+
142+
require.True(t, strings.HasPrefix(output, `"stacktrace":`), "Stacktrace added under an unexpected key.")
143+
assert.Contains(t, output[13:], "zap.TestStackField", "Expected stacktrace to contain caller.")
144+
}

logger_bench_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ func BenchmarkErrorField(b *testing.B) {
114114
})
115115
}
116116

117+
func BenchmarkStackField(b *testing.B) {
118+
withBenchedLogger(b, func(log zap.Logger) {
119+
log.Info("Error.", zap.Stack())
120+
})
121+
}
122+
117123
func BenchmarkObjectField(b *testing.B) {
118124
// Expect an extra allocation here, since casting the user struct to the
119125
// zap.Marshaler interface costs an alloc.

stacktrace.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2016 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package zap
22+
23+
import "runtime"
24+
25+
// takeStacktrace attempts to use the provided byte slice to take a stacktrace.
26+
// If the provided slice isn't large enough, takeStacktrace will allocate
27+
// successively larger slices until it can capture the whole stack.
28+
func takeStacktrace(buf []byte, includeAllGoroutines bool) string {
29+
if len(buf) == 0 {
30+
// We may have been passed a nil byte slice.
31+
buf = make([]byte, 1024)
32+
}
33+
n := runtime.Stack(buf, includeAllGoroutines)
34+
for n >= len(buf) {
35+
// Buffer wasn't large enough, allocate a larger one. No need to copy
36+
// previous buffer's contents.
37+
size := 2 * n
38+
buf = make([]byte, size)
39+
n = runtime.Stack(buf, includeAllGoroutines)
40+
}
41+
return string(buf[:n])
42+
}

stacktrace_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2016 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package zap
22+
23+
import (
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
)
28+
29+
func TestTakeStacktrace(t *testing.T) {
30+
// Even if we pass a tiny buffer, takeStacktrace should allocate until it
31+
// can capture the whole stacktrace.
32+
traceNil := takeStacktrace(nil, false)
33+
traceTiny := takeStacktrace(make([]byte, 1), false)
34+
for _, trace := range []string{traceNil, traceTiny} {
35+
// The top frame should be takeStacktrace.
36+
assert.Contains(t, trace, "zap.takeStacktrace", "Stacktrace should contain the takeStacktrace function.")
37+
// The stacktrace should also capture its immediate caller.
38+
assert.Contains(t, trace, "TestTakeStacktrace", "Stacktrace should contain the test function.")
39+
}
40+
}

0 commit comments

Comments
 (0)