diff --git a/klog.go b/klog.go index 6dabe706d..5a70e9729 100644 --- a/klog.go +++ b/klog.go @@ -748,6 +748,53 @@ func (l *loggingT) printWithFileLine(s severity, logr logr.InfoLogger, file stri l.output(s, logr, buf, file, line, alsoToStderr) } +// printS if loggr is specified, no need to output with logging module. If +// err arguments is specified, will call logr.Error, or output to errorLog severity +func (l *loggingT) printS(err error, loggr logr.Logger, msg string, keysAndValues ...interface{}) { + if loggr != nil { + if err != nil { + loggr.Error(err, msg, keysAndValues) + } else { + loggr.Info(msg, keysAndValues) + } + return + } + b := &bytes.Buffer{} + b.WriteString(fmt.Sprintf("%q", msg)) + if err != nil { + b.WriteByte(' ') + b.WriteString(fmt.Sprintf("err=%q", err.Error())) + } + kvListFormat(b, keysAndValues...) + var s severity + if err == nil { + s = infoLog + } else { + s = errorLog + } + l.printDepth(s, logging.logr, 1, b) +} + +const missingValue = "(MISSING)" + +func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { + for i := 0; i < len(keysAndValues); i += 2 { + var v interface{} + k := keysAndValues[i] + if i+1 < len(keysAndValues) { + v = keysAndValues[i+1] + } else { + v = missingValue + } + b.WriteByte(' ') + if _, ok := v.(fmt.Stringer); ok { + b.WriteString(fmt.Sprintf("%s=%q", k, v)) + } else { + b.WriteString(fmt.Sprintf("%s=%#v", k, v)) + } + } +} + // redirectBuffer is used to set an alternate destination for the logs type redirectBuffer struct { w io.Writer @@ -1241,6 +1288,18 @@ func (v Verbose) Infof(format string, args ...interface{}) { } } +// InfoS is equivalent to the global InfoS function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) InfoS(msg string, keysAndValues ...interface{}) { + if v.enabled { + if v.logr != nil { + v.logr.Info(msg, keysAndValues) + return + } + logging.printS(nil, nil, msg, keysAndValues...) + } +} + // Info logs to the INFO log. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Info(args ...interface{}) { @@ -1265,6 +1324,18 @@ func Infof(format string, args ...interface{}) { logging.printf(infoLog, logging.logr, format, args...) } +// InfoS structured logs to the INFO log. +// The msg argument used to add constant description to the log line. +// The key/value pairs would be join by "=" ; a newline is always appended. +// +// Basic examples: +// >> klog.InfoS("Pod status updated", "pod", "kubedns", "status", "ready") +// output: +// >> I1025 00:15:15.525108 1 controller_utils.go:116] "Pod status updated" pod="kubedns" status="ready" +func InfoS(msg string, keysAndValues ...interface{}) { + logging.printS(nil, logging.logr, msg, keysAndValues...) +} + // Warning logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Warning(args ...interface{}) { @@ -1313,6 +1384,19 @@ func Errorf(format string, args ...interface{}) { logging.printf(errorLog, logging.logr, format, args...) } +// ErrorS structured logs to the ERROR, WARNING, and INFO logs. +// the err argument used as "err" field of log line. +// The msg argument used to add constant description to the log line. +// The key/value pairs would be join by "=" ; a newline is always appended. +// +// Basic examples: +// >> klog.ErrorS(err, "Failed to update pod status") +// output: +// >> E1025 00:15:15.525108 1 controller_utils.go:114] "Failed to update pod status" err="timeout" +func ErrorS(err error, msg string, keysAndValues ...interface{}) { + logging.printS(err, logging.logr, msg, keysAndValues...) +} + // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, // including a stack trace of all running goroutines, then calls os.Exit(255). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. diff --git a/klog_test.go b/klog_test.go index 246172948..b6d4218a2 100644 --- a/klog_test.go +++ b/klog_test.go @@ -753,3 +753,125 @@ func TestKRef(t *testing.T) { }) } } + +// Test that InfoS works as advertised. +func TestInfoS(t *testing.T) { + setFlags() + defer logging.swap(logging.newBuffers()) + timeNow = func() time.Time { + return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local) + } + pid = 1234 + var testDataInfo = []struct { + msg string + format string + keysValues []interface{} + }{ + { + msg: "test", + format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" pod=\"kubedns\"\n", + keysValues: []interface{}{"pod", "kubedns"}, + }, + { + msg: "test", + format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" replicaNum=20\n", + keysValues: []interface{}{"replicaNum", 20}, + }, + } + + for _, data := range testDataInfo { + logging.file[infoLog] = &flushBuffer{} + InfoS(data.msg, data.keysValues...) + var line int + n, err := fmt.Sscanf(contents(infoLog), data.format, &line) + if n != 1 || err != nil { + t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog)) + } + want := fmt.Sprintf(data.format, line) + if contents(infoLog) != want { + t.Errorf("InfoS has wrong format: \n got:\t%s\nwant:\t%s", contents(infoLog), want) + } + } +} + +// Test that ErrorS works as advertised. +func TestErrorS(t *testing.T) { + setFlags() + defer logging.swap(logging.newBuffers()) + timeNow = func() time.Time { + return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local) + } + logging.logFile = "" + pid = 1234 + ErrorS(fmt.Errorf("update status failed"), "Failed to update pod status", "pod", "kubedns") + var line int + format := "E0102 15:04:05.067890 1234 klog_test.go:%d] \"Failed to update pod status\" err=\"update status failed\" pod=\"kubedns\"\n" + n, err := fmt.Sscanf(contents(errorLog), format, &line) + if n != 1 || err != nil { + t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(errorLog)) + } + want := fmt.Sprintf(format, line) + if contents(errorLog) != want { + t.Errorf("ErrorS has wrong format: \n got:\t%s\nwant:\t%s", contents(errorLog), want) + } +} + +// Test that kvListFormat works as advertised. +func TestKvListFormat(t *testing.T) { + var testKVList = []struct { + keysValues []interface{} + want string + }{ + { + keysValues: []interface{}{"pod", "kubedns"}, + want: " pod=\"kubedns\"", + }, + { + keysValues: []interface{}{"pod", "kubedns", "update", true}, + want: " pod=\"kubedns\" update=true", + }, + { + keysValues: []interface{}{"pod", "kubedns", "spec", struct { + X int + Y string + }{X: 76, Y: "strval"}}, + want: " pod=\"kubedns\" spec=struct { X int; Y string }{X:76, Y:\"strval\"}", + }, + { + keysValues: []interface{}{"pod", "kubedns", "values", []int{8, 6, 7, 5, 3, 0, 9}}, + want: " pod=\"kubedns\" values=[]int{8, 6, 7, 5, 3, 0, 9}", + }, + { + keysValues: []interface{}{"pod", "kubedns", "values", []string{"deployment", "svc", "configmap"}}, + want: " pod=\"kubedns\" values=[]string{\"deployment\", \"svc\", \"configmap\"}", + }, + { + keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}}, + want: " pod=\"kubedns\" maps=map[string]int{\"three\":4}", + }, + { + keysValues: []interface{}{"pod", KRef("kube-system", "kubedns"), "status", "ready"}, + want: " pod=\"kube-system/kubedns\" status=\"ready\"", + }, + { + keysValues: []interface{}{"pod", KRef("", "kubedns"), "status", "ready"}, + want: " pod=\"kubedns\" status=\"ready\"", + }, + { + keysValues: []interface{}{"pod", KObj(mockKmeta{"test-name", "test-ns"}), "status", "ready"}, + want: " pod=\"test-ns/test-name\" status=\"ready\"", + }, + { + keysValues: []interface{}{"pod", KObj(mockKmeta{"test-name", ""}), "status", "ready"}, + want: " pod=\"test-name\" status=\"ready\"", + }, + } + + for _, d := range testKVList { + b := &bytes.Buffer{} + kvListFormat(b, d.keysValues...) + if b.String() != d.want { + t.Errorf("kvlist format error:\n got:\n\t%s\nwant:\t%s", b.String(), d.want) + } + } +}