go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/logging/sdlogger/logger_test.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sdlogger 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "regexp" 24 "testing" 25 26 "cloud.google.com/go/errorreporting" 27 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/logging" 31 32 . "github.com/smartystreets/goconvey/convey" 33 ) 34 35 func use(ctx context.Context, out io.Writer, proto LogEntry) context.Context { 36 return logging.SetFactory(ctx, Factory(&Sink{Out: out}, proto, nil)) 37 } 38 39 func read(b *bytes.Buffer) *LogEntry { 40 var result LogEntry 41 if err := json.NewDecoder(b).Decode(&result); err != nil { 42 panic(fmt.Errorf("could not decode `%s`: %q", b.Bytes(), err)) 43 } 44 return &result 45 } 46 47 func TestLogger(t *testing.T) { 48 t.Parallel() 49 50 c := context.Background() 51 c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC) 52 buf := bytes.NewBuffer([]byte{}) 53 54 Convey("Basic", t, func() { 55 c = use(c, buf, LogEntry{TraceID: "hi"}) 56 logging.Infof(c, "test context") 57 So(read(buf), ShouldResemble, &LogEntry{ 58 Message: "test context", 59 Severity: InfoSeverity, 60 Timestamp: Timestamp{Seconds: 1454472306, Nanos: 7}, 61 TraceID: "hi", // copied from the prototype 62 }) 63 }) 64 65 Convey("Simple fields", t, func() { 66 c = use(c, buf, LogEntry{}) 67 logging.NewFields(map[string]any{"foo": "bar"}).Infof(c, "test field") 68 e := read(buf) 69 So(e.Fields["foo"], ShouldEqual, "bar") 70 So(e.Message, ShouldEqual, `test field :: {"foo":"bar"}`) 71 }) 72 73 Convey("Error field", t, func() { 74 c = use(c, buf, LogEntry{}) 75 c = logging.SetField(c, "foo", "bar") 76 logging.WithError(fmt.Errorf("boom")).Infof(c, "boom") 77 e := read(buf) 78 So(e.Fields["foo"], ShouldEqual, "bar") // still works 79 So(e.Fields[logging.ErrorKey], ShouldEqual, "boom") // also works 80 So(e.Message, ShouldEqual, `boom :: {"error":"boom", "foo":"bar"}`) 81 }) 82 } 83 84 type fakeCloudErrorsSink struct { 85 CloudErrorsSink 86 errRptEntry *errorreporting.Entry 87 } 88 89 func (f *fakeCloudErrorsSink) Write(l *LogEntry) { 90 if l.Severity == ErrorSeverity { 91 errRptEntry := prepErrorReportingEntry(l, nil) 92 f.errRptEntry = &errRptEntry 93 } 94 f.Out.Write(l) 95 } 96 97 func newFakeCloudErrorsSink(out io.Writer) *fakeCloudErrorsSink { 98 return &fakeCloudErrorsSink{CloudErrorsSink: CloudErrorsSink{Out: &Sink{Out: out}}} 99 } 100 101 func useLog(ctx context.Context, fakeSink *fakeCloudErrorsSink, proto LogEntry) context.Context { 102 return logging.SetFactory(ctx, Factory(fakeSink, proto, nil)) 103 } 104 105 func TestErrorReporting(t *testing.T) { 106 t.Parallel() 107 108 Convey("errStackRe regex match", t, func() { 109 errStr := "original error: rpc error: code = Internal desc = internal: attaching a status: rpc error: code = FailedPrecondition desc = internal" 110 stackStr := `goroutine 27693: 111 #0 go.chromium.org/luci/grpc/appstatus/status.go:59 - appstatus.Attach() 112 reason: attaching a status 113 tag["application-specific response status"]: &status.Status{s:(*status.Status)(0xc002885e60)} 114 ` 115 msg := errStr + "\n\n" + stackStr 116 match := errStackRe.FindStringSubmatch(msg) 117 So(match, ShouldNotBeNil) 118 So(match[1], ShouldEqual, errStr) 119 So(match[2], ShouldEqual, stackStr) 120 }) 121 122 Convey("end to end", t, func() { 123 c := context.Background() 124 c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC) 125 buf := bytes.NewBuffer([]byte{}) 126 127 Convey("logging error with full stack", func() { 128 fakeErrSink := newFakeCloudErrorsSink(buf) 129 c = useLog(c, fakeErrSink, LogEntry{TraceID: "trace123"}) 130 131 errors.Log(c, errors.New("test error")) 132 133 // assert errorreporting.entry has the stack from errors.renderStack(). 134 So(fakeErrSink.errRptEntry.Error.Error(), ShouldEqual, "original error: test error (Log Trace ID: trace123)") 135 stackMatch, err := regexp.MatchString(`goroutine \d+:\n.*sdlogger.TestErrorReporting.func*`, string(fakeErrSink.errRptEntry.Stack)) 136 So(err, ShouldBeNil) 137 So(stackMatch, ShouldBeTrue) 138 139 // assert outputted LogEntry.message 140 logOutput := read(buf) 141 logMsgMatch, err := regexp.MatchString(`original error: test error\n\ngoroutine \d+:\n.*sdlogger.TestErrorReporting.func*`, logOutput.Message) 142 So(err, ShouldBeNil) 143 So(logMsgMatch, ShouldBeTrue) 144 }) 145 146 Convey("logging error without stack", func() { 147 fakeErrSink := newFakeCloudErrorsSink(buf) 148 c = useLog(c, fakeErrSink, LogEntry{TraceID: "trace123"}) 149 150 logging.Errorf(c, "test error") 151 152 So(fakeErrSink.errRptEntry.Error.Error(), ShouldEqual, "test error (Log Trace ID: trace123)") 153 So(fakeErrSink.errRptEntry.Stack, ShouldNotBeNil) 154 So(read(buf), ShouldResemble, &LogEntry{ 155 Message: "test error", 156 Severity: ErrorSeverity, 157 Timestamp: Timestamp{Seconds: 1454472306, Nanos: 7}, 158 TraceID: "trace123", 159 }) 160 }) 161 162 Convey("logging non-error", func() { 163 fakeErrSink := newFakeCloudErrorsSink(buf) 164 c = useLog(c, fakeErrSink, LogEntry{TraceID: "trace123"}) 165 166 logging.Infof(c, "info") 167 168 So(fakeErrSink.errRptEntry, ShouldBeNil) 169 So(read(buf), ShouldResemble, &LogEntry{ 170 Message: "info", 171 Severity: InfoSeverity, 172 Timestamp: Timestamp{Seconds: 1454472306, Nanos: 7}, 173 TraceID: "trace123", 174 }) 175 }) 176 }) 177 } 178 179 func TestCleanupStack(t *testing.T) { 180 t.Parallel() 181 182 call := func(s string) string { 183 return string(cleanupStack([]byte(s))) 184 } 185 186 Convey("Works", t, func() { 187 stack := `goroutine 19 [running]: 188 go.chromium.org/luci/common/logging/sdlogger.prepErrorReportingEntry(0xc0001abea0, 0x0) 189 zzz/go.chromium.org/luci/common/logging/sdlogger/logger.go:210 +0x1d9 190 go.chromium.org/luci/common/logging/sdlogger.(*fakeCloudErrorsSink).Write(0xc0001913b0, 0xc0001abea0) 191 zzz/infra/go/src/go.chromium.org/luci/common/logging/sdlogger/logger_test.go:91 +0x65 192 go.chromium.org/luci/common/logging/sdlogger.(*jsonLogger).LogCall(0xc0001da0c0, 0x3, 0x0?, {0x1683678, 0xa}, {0x0, 0x0, 0x0}) 193 zzz/infra/go/src/go.chromium.org/luci/common/logging/sdlogger/logger.go:311 +0x2f2 194 go.chromium.org/luci/common/logging.Errorf({0x176d680?, 0xc0001913e0?}, {0x1683678, 0xa}, {0x0, 0x0, 0x0}) 195 zzz/infra/go/src/go.chromium.org/luci/common/logging/exported.go:50 +0x71 196 go.chromium.org/luci/common/some/package.SomeCall.func2.2() 197 zzz/infra/go/src/go.chromium.org/luci/common/some/package/file.go:150 +0x166 198 ` 199 200 expected := `goroutine 19 [running]: 201 go.chromium.org/luci/common/some/package.SomeCall.func2.2() 202 zzz/infra/go/src/go.chromium.org/luci/common/some/package/file.go:150 +0x166 203 ` 204 205 So(call(stack), ShouldEqual, expected) 206 }) 207 208 Convey("Skips unexpected stuff", t, func() { 209 So(call(""), ShouldEqual, "") 210 So(call("abc"), ShouldEqual, "abc") 211 So(call("abc\ndef"), ShouldEqual, "abc\ndef") 212 }) 213 } 214 215 // A fuzz test to ensure cleanupStack doesn't panic. 216 217 func FuzzCleanupStack(f *testing.F) { 218 stack := `goroutine 19 [running]: 219 go.chromium.org/luci/common/logging/sdlogger.prepErrorReportingEntry(0xc0001abea0, 0x0) 220 zzz/go.chromium.org/luci/common/logging/sdlogger/logger.go:210 +0x1d9 221 go.chromium.org/luci/common/logging/sdlogger.(*fakeCloudErrorsSink).Write(0xc0001913b0, 0xc0001abea0) 222 zzz/infra/go/src/go.chromium.org/luci/common/logging/sdlogger/logger_test.go:91 +0x65 223 go.chromium.org/luci/common/logging/sdlogger.(*jsonLogger).LogCall(0xc0001da0c0, 0x3, 0x0?, {0x1683678, 0xa}, {0x0, 0x0, 0x0}) 224 zzz/infra/go/src/go.chromium.org/luci/common/logging/sdlogger/logger.go:311 +0x2f2 225 go.chromium.org/luci/common/logging.Errorf({0x176d680?, 0xc0001913e0?}, {0x1683678, 0xa}, {0x0, 0x0, 0x0}) 226 zzz/infra/go/src/go.chromium.org/luci/common/logging/exported.go:50 +0x71 227 go.chromium.org/luci/common/some/package.SomeCall.func2.2() 228 zzz/infra/go/src/go.chromium.org/luci/common/some/package/file.go:150 +0x166 229 ` 230 f.Add(stack) 231 f.Fuzz(func(t *testing.T, s string) { 232 out := string(cleanupStack([]byte(s))) 233 switch { 234 case len(out) > len(s): 235 t.Errorf("output is larger than input:\n%q\n%q", s, out) 236 case len(out) == len(s): 237 if out != s { 238 t.Errorf("unexpected mutation:\n%q\n%q", s, out) 239 } 240 } 241 }) 242 }