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