summaryrefslogtreecommitdiffstats
path: root/dependencies/pkg/mod/go.uber.org/zap@v1.23.0/stacktrace_ext_test.go
blob: 71f098333495ee2c663c84af89eb87b5ac52aac2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright (c) 2016, 2017 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zap_test

import (
	"bytes"
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// _zapPackages are packages that we search for in the logging output to match a
// zap stack frame. It is different from _zapStacktracePrefixes which  is only
// intended to match on the function name, while this is on the full output
// which includes filenames.
var _zapPackages = []string{
	"go.uber.org/zap.",
	"go.uber.org/zap/zapcore.",
}

func TestStacktraceFiltersZapLog(t *testing.T) {
	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
		logger.Error("test log")
		logger.Sugar().Error("sugar test log")

		require.Contains(t, out.String(), "TestStacktraceFiltersZapLog", "Should not strip out non-zap import")
		verifyNoZap(t, out.String())
	})
}

func TestStacktraceFiltersZapMarshal(t *testing.T) {
	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
		marshal := func(enc zapcore.ObjectEncoder) error {
			logger.Warn("marshal caused warn")
			enc.AddString("f", "v")
			return nil
		}
		logger.Error("test log", zap.Object("obj", zapcore.ObjectMarshalerFunc(marshal)))

		logs := out.String()

		// The marshal function (which will be under the test function) should not be stripped.
		const marshalFnPrefix = "TestStacktraceFiltersZapMarshal."
		require.Contains(t, logs, marshalFnPrefix, "Should not strip out marshal call")

		// There should be no zap stack traces before that point.
		marshalIndex := strings.Index(logs, marshalFnPrefix)
		verifyNoZap(t, logs[:marshalIndex])

		// After that point, there should be zap stack traces - we don't want to strip out
		// the Marshal caller information.
		for _, fnPrefix := range _zapPackages {
			require.Contains(t, logs[marshalIndex:], fnPrefix, "Missing zap caller stack for Marshal")
		}
	})
}

func TestStacktraceFiltersVendorZap(t *testing.T) {
	// We already have the dependencies downloaded so this should be
	// instant.
	deps := downloadDependencies(t)

	// We need to simulate a zap as a vendor library, so we're going to
	// create a fake GOPATH and run the above test which will contain zap
	// in the vendor directory.
	withGoPath(t, func(goPath string) {
		zapDir, err := os.Getwd()
		require.NoError(t, err, "Failed to get current directory")

		testDir := filepath.Join(goPath, "src/go.uber.org/zap_test/")
		vendorDir := filepath.Join(testDir, "vendor")
		require.NoError(t, os.MkdirAll(testDir, 0777), "Failed to create source director")

		curFile := getSelfFilename(t)
		setupSymlink(t, curFile, filepath.Join(testDir, curFile))

		// Set up symlinks for zap, and for any test dependencies.
		setupSymlink(t, zapDir, filepath.Join(vendorDir, "go.uber.org/zap"))
		for _, dep := range deps {
			setupSymlink(t, dep.Dir, filepath.Join(vendorDir, dep.ImportPath))
		}

		// Now run the above test which ensures we filter out zap
		// stacktraces, but this time zap is in a vendor
		cmd := exec.Command("go", "test", "-v", "-run", "TestStacktraceFiltersZap")
		cmd.Dir = testDir
		cmd.Env = append(os.Environ(), "GO111MODULE=off")
		out, err := cmd.CombinedOutput()
		require.NoError(t, err, "Failed to run test in vendor directory, output: %s", out)
		assert.Contains(t, string(out), "PASS")
	})
}

func TestStacktraceWithoutCallerSkip(t *testing.T) {
	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
		func() {
			logger.Error("test log")
		}()

		require.Contains(t, out.String(), "TestStacktraceWithoutCallerSkip.", "Should not skip too much")
		verifyNoZap(t, out.String())
	})
}

func TestStacktraceWithCallerSkip(t *testing.T) {
	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
		logger = logger.WithOptions(zap.AddCallerSkip(2))
		func() {
			logger.Error("test log")
		}()

		require.NotContains(t, out.String(), "TestStacktraceWithCallerSkip.", "Should skip as requested by caller skip")
		require.Contains(t, out.String(), "TestStacktraceWithCallerSkip", "Should not skip too much")
		verifyNoZap(t, out.String())
	})
}

// withLogger sets up a logger with a real encoder set up, so that any marshal functions are called.
// The inbuilt observer does not call Marshal for objects/arrays, which we need for some tests.
func withLogger(t *testing.T, fn func(logger *zap.Logger, out *bytes.Buffer)) {
	buf := &bytes.Buffer{}
	encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
	core := zapcore.NewCore(encoder, zapcore.AddSync(buf), zapcore.DebugLevel)
	logger := zap.New(core, zap.AddStacktrace(zap.DebugLevel))
	fn(logger, buf)
}

func verifyNoZap(t *testing.T, logs string) {
	for _, fnPrefix := range _zapPackages {
		require.NotContains(t, logs, fnPrefix, "Should not strip out marshal call")
	}
}

func withGoPath(t *testing.T, f func(goPath string)) {
	goPath := filepath.Join(t.TempDir(), "gopath")
	t.Setenv("GOPATH", goPath)

	f(goPath)
}

func getSelfFilename(t *testing.T) string {
	_, file, _, ok := runtime.Caller(0)
	require.True(t, ok, "Failed to get caller information to identify local file")

	return filepath.Base(file)
}

func setupSymlink(t *testing.T, src, dst string) {
	// Make sure the destination directory exists.
	os.MkdirAll(filepath.Dir(dst), 0777)

	// Get absolute path of the source for the symlink, otherwise we can create a symlink
	// that uses relative paths.
	srcAbs, err := filepath.Abs(src)
	require.NoError(t, err, "Failed to get absolute path")

	require.NoError(t, os.Symlink(srcAbs, dst), "Failed to set up symlink")
}

type dependency struct {
	ImportPath string `json:"Path"` // import path of the dependency
	Dir        string `json:"Dir"`  // location on disk
}

// Downloads all dependencies for the current Go module and reports their
// module paths and locations on disk.
func downloadDependencies(t *testing.T) []dependency {
	cmd := exec.Command("go", "mod", "download", "-json")

	stdout, err := cmd.Output()
	require.NoError(t, err, "Failed to run 'go mod download'")

	var deps []dependency
	dec := json.NewDecoder(bytes.NewBuffer(stdout))
	for dec.More() {
		var d dependency
		require.NoError(t, dec.Decode(&d), "Failed to decode dependency")
		deps = append(deps, d)
	}

	return deps
}