testutils/logger.go (147 lines of code) (raw):

// Copyright (c) 2015 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 testutils import ( "bytes" "fmt" "os" "strings" "sync" "testing" "time" "github.com/uber/tchannel-go" "go.uber.org/atomic" ) // writer is shared between multiple loggers, and serializes acccesses to // the underlying buffer. type writer struct { sync.Mutex buf *bytes.Buffer } // testLogger is a logger that writes all output to a buffer, and can report // the logs if the test has failed. type testLogger struct { t testing.TB fields tchannel.LogFields w *writer } type errorLoggerState struct { matchCount []atomic.Uint32 } type errorLogger struct { tchannel.Logger t testing.TB v *LogVerification s *errorLoggerState } func newWriter() *writer { return &writer{buf: &bytes.Buffer{}} } func (w *writer) withLock(f func(*bytes.Buffer)) { w.Lock() f(w.buf) w.Unlock() } // Matches returns true if the message and fields match the filter. func (f LogFilter) Matches(msg string, fields tchannel.LogFields) bool { // First check the message and ensure it contains Filter if !strings.Contains(msg, f.Filter) { return false } // if there are no field filters, then the message match is enough. if len(f.FieldFilters) == 0 { return true } fieldsMap := make(map[string]interface{}) for _, field := range fields { fieldsMap[field.Key] = field.Value } for k, filter := range f.FieldFilters { value, ok := fieldsMap[k] if !ok { return false } if !strings.Contains(fmt.Sprint(value), filter) { return false } } return true } func newTestLogger(t testing.TB) testLogger { return testLogger{t, nil, newWriter()} } func (l testLogger) Enabled(level tchannel.LogLevel) bool { return true } func (l testLogger) log(prefix string, msg string) { logLine := fmt.Sprintf("%s [%v] %v %v\n", time.Now().Format("15:04:05.000000"), prefix, msg, l.Fields()) l.w.withLock(func(w *bytes.Buffer) { w.WriteString(logLine) }) } func (l testLogger) Fatal(msg string) { l.log("F", msg) } func (l testLogger) Error(msg string) { l.log("E", msg) } func (l testLogger) Warn(msg string) { l.log("W", msg) } func (l testLogger) Info(msg string) { l.log("I", msg) } func (l testLogger) Infof(msg string, args ...interface{}) { l.log("I", fmt.Sprintf(msg, args...)) } func (l testLogger) Debug(msg string) { l.log("D", msg) } func (l testLogger) Debugf(msg string, args ...interface{}) { l.log("D", fmt.Sprintf(msg, args...)) } func (l testLogger) Fields() tchannel.LogFields { return l.fields } func (l testLogger) WithFields(fields ...tchannel.LogField) tchannel.Logger { existing := len(l.Fields()) newFields := make(tchannel.LogFields, existing+len(fields)) copy(newFields, l.Fields()) copy(newFields[existing:], fields) return testLogger{l.t, newFields, l.w} } func (l testLogger) report() { if os.Getenv("LOGS_ON_FAILURE") == "" { return } if l.t.Failed() { l.w.withLock(func(w *bytes.Buffer) { l.t.Logf("Debug logs:\n%s", w.String()) }) } } // checkFilters returns whether the message can be ignored by the filters. func (l errorLogger) checkFilters(msg string) bool { match := -1 for i, filter := range l.v.Filters { if filter.Matches(msg, l.Fields()) { match = i } } if match == -1 { return false } matchCount := l.s.matchCount[match].Inc() return uint(matchCount) <= l.v.Filters[match].Count } func (l errorLogger) checkErr(prefix, msg string) { if l.checkFilters(msg) { return } l.t.Errorf("Unexpected log: %v: %s %v", prefix, msg, l.Logger.Fields()) } func (l errorLogger) Fatal(msg string) { l.checkErr("[Fatal]", msg) l.Logger.Fatal(msg) } func (l errorLogger) Error(msg string) { l.checkErr("[Error]", msg) l.Logger.Error(msg) } func (l errorLogger) Warn(msg string) { l.checkErr("[Warn]", msg) l.Logger.Warn(msg) } func (l errorLogger) WithFields(fields ...tchannel.LogField) tchannel.Logger { return errorLogger{l.Logger.WithFields(fields...), l.t, l.v, l.s} }