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  }