1// Copyright (c) 2016, 2017 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
21package zap_test
22
23import (
24	"bytes"
25	"encoding/json"
26	"io/ioutil"
27	"os"
28	"os/exec"
29	"path/filepath"
30	"runtime"
31	"strings"
32	"testing"
33
34	"go.uber.org/zap"
35	"go.uber.org/zap/zapcore"
36
37	"github.com/stretchr/testify/assert"
38	"github.com/stretchr/testify/require"
39)
40
41// _zapPackages are packages that we search for in the logging output to match a
42// zap stack frame. It is different from _zapStacktracePrefixes which  is only
43// intended to match on the function name, while this is on the full output
44// which includes filenames.
45var _zapPackages = []string{
46	"go.uber.org/zap.",
47	"go.uber.org/zap/zapcore.",
48}
49
50func TestStacktraceFiltersZapLog(t *testing.T) {
51	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
52		logger.Error("test log")
53		logger.Sugar().Error("sugar test log")
54
55		require.Contains(t, out.String(), "TestStacktraceFiltersZapLog", "Should not strip out non-zap import")
56		verifyNoZap(t, out.String())
57	})
58}
59
60func TestStacktraceFiltersZapMarshal(t *testing.T) {
61	withLogger(t, func(logger *zap.Logger, out *bytes.Buffer) {
62		marshal := func(enc zapcore.ObjectEncoder) error {
63			logger.Warn("marshal caused warn")
64			enc.AddString("f", "v")
65			return nil
66		}
67		logger.Error("test log", zap.Object("obj", zapcore.ObjectMarshalerFunc(marshal)))
68
69		logs := out.String()
70
71		// The marshal function (which will be under the test function) should not be stripped.
72		const marshalFnPrefix = "TestStacktraceFiltersZapMarshal."
73		require.Contains(t, logs, marshalFnPrefix, "Should not strip out marshal call")
74
75		// There should be no zap stack traces before that point.
76		marshalIndex := strings.Index(logs, marshalFnPrefix)
77		verifyNoZap(t, logs[:marshalIndex])
78
79		// After that point, there should be zap stack traces - we don't want to strip out
80		// the Marshal caller information.
81		for _, fnPrefix := range _zapPackages {
82			require.Contains(t, logs[marshalIndex:], fnPrefix, "Missing zap caller stack for Marshal")
83		}
84	})
85}
86
87func TestStacktraceFiltersVendorZap(t *testing.T) {
88	// We already have the dependencies downloaded so this should be
89	// instant.
90	deps := downloadDependencies(t)
91
92	// We need to simulate a zap as a vendor library, so we're going to
93	// create a fake GOPATH and run the above test which will contain zap
94	// in the vendor directory.
95	withGoPath(t, func(goPath string) {
96		zapDir, err := os.Getwd()
97		require.NoError(t, err, "Failed to get current directory")
98
99		testDir := filepath.Join(goPath, "src/go.uber.org/zap_test/")
100		vendorDir := filepath.Join(testDir, "vendor")
101		require.NoError(t, os.MkdirAll(testDir, 0777), "Failed to create source director")
102
103		curFile := getSelfFilename(t)
104		setupSymlink(t, curFile, filepath.Join(testDir, curFile))
105
106		// Set up symlinks for zap, and for any test dependencies.
107		setupSymlink(t, zapDir, filepath.Join(vendorDir, "go.uber.org/zap"))
108		for _, dep := range deps {
109			setupSymlink(t, dep.Dir, filepath.Join(vendorDir, dep.ImportPath))
110		}
111
112		// Now run the above test which ensures we filter out zap
113		// stacktraces, but this time zap is in a vendor
114		cmd := exec.Command("go", "test", "-v", "-run", "TestStacktraceFiltersZap")
115		cmd.Dir = testDir
116		cmd.Env = append(os.Environ(), "GO111MODULE=off")
117		out, err := cmd.CombinedOutput()
118		require.NoError(t, err, "Failed to run test in vendor directory, output: %s", out)
119		assert.Contains(t, string(out), "PASS")
120	})
121}
122
123// withLogger sets up a logger with a real encoder set up, so that any marshal functions are called.
124// The inbuilt observer does not call Marshal for objects/arrays, which we need for some tests.
125func withLogger(t *testing.T, fn func(logger *zap.Logger, out *bytes.Buffer)) {
126	buf := &bytes.Buffer{}
127	encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
128	core := zapcore.NewCore(encoder, zapcore.AddSync(buf), zapcore.DebugLevel)
129	logger := zap.New(core, zap.AddStacktrace(zap.DebugLevel))
130	fn(logger, buf)
131}
132
133func verifyNoZap(t *testing.T, logs string) {
134	for _, fnPrefix := range _zapPackages {
135		require.NotContains(t, logs, fnPrefix, "Should not strip out marshal call")
136	}
137}
138
139func withGoPath(t *testing.T, f func(goPath string)) {
140	goPath, err := ioutil.TempDir("", "gopath")
141	require.NoError(t, err, "Failed to create temporary directory for GOPATH")
142	//defer os.RemoveAll(goPath)
143
144	os.Setenv("GOPATH", goPath)
145	defer os.Setenv("GOPATH", os.Getenv("GOPATH"))
146
147	f(goPath)
148}
149
150func getSelfFilename(t *testing.T) string {
151	_, file, _, ok := runtime.Caller(0)
152	require.True(t, ok, "Failed to get caller information to identify local file")
153
154	return filepath.Base(file)
155}
156
157func setupSymlink(t *testing.T, src, dst string) {
158	// Make sure the destination directory exists.
159	os.MkdirAll(filepath.Dir(dst), 0777)
160
161	// Get absolute path of the source for the symlink, otherwise we can create a symlink
162	// that uses relative paths.
163	srcAbs, err := filepath.Abs(src)
164	require.NoError(t, err, "Failed to get absolute path")
165
166	require.NoError(t, os.Symlink(srcAbs, dst), "Failed to set up symlink")
167}
168
169type dependency struct {
170	ImportPath string `json:"Path"` // import path of the dependency
171	Dir        string `json:"Dir"`  // location on disk
172}
173
174// Downloads all dependencies for the current Go module and reports their
175// module paths and locations on disk.
176func downloadDependencies(t *testing.T) []dependency {
177	cmd := exec.Command("go", "mod", "download", "-json")
178
179	stdout, err := cmd.Output()
180	require.NoError(t, err, "Failed to run 'go mod download'")
181
182	var deps []dependency
183	dec := json.NewDecoder(bytes.NewBuffer(stdout))
184	for dec.More() {
185		var d dependency
186		require.NoError(t, dec.Decode(&d), "Failed to decode dependency")
187		deps = append(deps, d)
188	}
189
190	return deps
191}
192