From 567ef13033a7916bdca02fd1b572b97fcc8166a0 Mon Sep 17 00:00:00 2001
From: Davanum Srinivas
Date: Wed, 2 Jun 2021 19:55:30 -0400
Subject: [PATCH 001/125] Update to newer golang versions
Signed-off-by: Davanum Srinivas
---
.github/workflows/test.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1d3a14801..2aa4fb29a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
- go-versions: [1.12.x, 1.13.x, 1.14.x]
+ go-versions: [1.14.x, 1.15.x, 1.16.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
From 6055fa4f0d901c8d12a9fc95452bbc75224c32dc Mon Sep 17 00:00:00 2001
From: Solly Ross
Date: Mon, 14 Jun 2021 15:14:56 -0700
Subject: [PATCH 002/125] Remove directxman12 from OWNERS
As per
https://groups.google.com/g/kubebuilder/c/-5hOFa_adr4/m/Sp_Zb755AwAJ,
she is stepping down.
---
OWNERS | 1 -
1 file changed, 1 deletion(-)
diff --git a/OWNERS b/OWNERS
index 380e514f2..f689c1641 100644
--- a/OWNERS
+++ b/OWNERS
@@ -15,5 +15,4 @@ approvers:
- tallclair
- piosz
- brancz
- - DirectXMan12
- lavalamp
From 0cd82b3e2ba737784f2d9664c8914aa6922741a7 Mon Sep 17 00:00:00 2001
From: Umanga Chapagain
Date: Mon, 21 Jun 2021 12:34:00 +0530
Subject: [PATCH 003/125] logcheck: add flag to allow unstructured logs
By default, logcheck will check for use of unstructured logging
and use of incorrect structured logging pattern, and
report errors.
allow-unstructured flag can be passed to logcheck to suppress
errors when unstructured logging is used. It is usefull in
cases when we want to test for correct use of structured
logging patterns but not enfore it.
Signed-off-by: Umanga Chapagain
---
hack/tools/logcheck/main.go | 38 ++++++++++++++++++++++++-------------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/hack/tools/logcheck/main.go b/hack/tools/logcheck/main.go
index 27028dcd1..f82f6ea0d 100644
--- a/hack/tools/logcheck/main.go
+++ b/hack/tools/logcheck/main.go
@@ -17,6 +17,7 @@ limitations under the License.
package main
import (
+ "flag"
"fmt"
"go/ast"
"go/token"
@@ -27,21 +28,32 @@ import (
"golang.org/x/tools/go/analysis/singlechecker"
)
-// Doc explaining the tool.
-const Doc = "Tool to check use of unstructured logging patterns."
-
-// Analyzer runs static analysis.
-var Analyzer = &analysis.Analyzer{
- Name: "logcheck",
- Doc: Doc,
- Run: run,
+type config struct {
+ // When enabled, logcheck will ignore calls to unstructured klog methods (Info, Infof, Error, Errorf, Warningf, etc)
+ allowUnstructured bool
}
func main() {
- singlechecker.Main(Analyzer)
+ singlechecker.Main(analyser())
+}
+
+func analyser() *analysis.Analyzer {
+ c := config{}
+ logcheckFlags := flag.NewFlagSet("", flag.ExitOnError)
+ logcheckFlags.BoolVar(&c.allowUnstructured, "allow-unstructured", c.allowUnstructured, `when enabled, logcheck will ignore calls to unstructured
+klog methods (Info, Infof, Error, Errorf, Warningf, etc)`)
+
+ return &analysis.Analyzer{
+ Name: "logcheck",
+ Doc: "Tool to check use of unstructured logging patterns.",
+ Run: func(pass *analysis.Pass) (interface{}, error) {
+ return run(pass, &c)
+ },
+ Flags: *logcheckFlags,
+ }
}
-func run(pass *analysis.Pass) (interface{}, error) {
+func run(pass *analysis.Pass, c *config) (interface{}, error) {
for _, file := range pass.Files {
@@ -50,7 +62,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
// We are intrested in function calls, as we want to detect klog.* calls
// passing all function calls to checkForFunctionExpr
if fexpr, ok := n.(*ast.CallExpr); ok {
- checkForFunctionExpr(fexpr, pass)
+ checkForFunctionExpr(fexpr, pass, c)
}
return true
@@ -60,7 +72,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
}
// checkForFunctionExpr checks for unstructured logging function, prints error if found any.
-func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass) {
+func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) {
fun := fexpr.Fun
args := fexpr.Args
@@ -98,7 +110,7 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass) {
} else if fName == "ErrorS" {
isKeysValid(args[2:], fun, pass, fName)
}
- } else {
+ } else if !c.allowUnstructured {
msg := fmt.Sprintf("unstructured logging function %q should not be used", fName)
pass.Report(analysis.Diagnostic{
Pos: fun.Pos(),
From d6ef17a0b077c332e8fffb802688e275b85b424e Mon Sep 17 00:00:00 2001
From: Umanga Chapagain
Date: Mon, 21 Jun 2021 13:11:17 +0530
Subject: [PATCH 004/125] logcheck: add unit tests for different flags
Signed-off-by: Umanga Chapagain
---
hack/tools/logcheck/main_test.go | 24 ++++++-
.../allowUnstructuredLogs.go | 63 +++++++++++++++++++
.../doNotAllowUnstructuredLogs.go} | 43 +++++++------
3 files changed, 110 insertions(+), 20 deletions(-)
create mode 100644 hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
rename hack/tools/logcheck/testdata/{data.go => src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go} (88%)
diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go
index b41f541b4..3e0c08ae3 100644
--- a/hack/tools/logcheck/main_test.go
+++ b/hack/tools/logcheck/main_test.go
@@ -23,5 +23,27 @@ import (
)
func TestAnalyzer(t *testing.T) {
- analysistest.Run(t, analysistest.TestData(), Analyzer)
+ tests := []struct {
+ name string
+ allowUnstructured string
+ testPackage string
+ }{
+ {
+ name: "Allow unstructured logs",
+ allowUnstructured: "true",
+ testPackage: "allowUnstructuredLogs",
+ },
+ {
+ name: "Do not allow unstructured logs",
+ allowUnstructured: "false",
+ testPackage: "doNotAllowUnstructuredLogs",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ analyzer := analyser()
+ analyzer.Flags.Set("allow-unstructured", tt.allowUnstructured)
+ analysistest.Run(t, analysistest.TestData(), analyzer, tt.testPackage)
+ })
+ }
}
diff --git a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
new file mode 100644
index 000000000..346410d52
--- /dev/null
+++ b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This fake package is created as golang.org/x/tools/go/analysis/analysistest
+// expects it to be here for loading. This package is used to test allow-unstructured
+// flag which suppresses errors when unstructured logging is used.
+// This is a test file for unstructured logging static check tool unit tests.
+
+package allowUnstructuredLogs
+
+import (
+ klog "k8s.io/klog/v2"
+)
+
+func allowUnstructuredLogs() {
+ // Structured logs
+ // Error is expected if structured logging pattern is not used correctly
+ klog.InfoS("test log")
+ klog.ErrorS(nil, "test log")
+ klog.InfoS("Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to InfoS should always be Key Value pairs. Please check if there is any key or value missing.`
+ klog.ErrorS(nil, "Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to ErrorS should always be Key Value pairs. Please check if there is any key or value missing.`
+ klog.InfoS("Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
+ klog.ErrorS(nil, "Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
+ klog.InfoS("Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
+ klog.ErrorS(nil, "Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
+ klog.InfoS("Starting container in a pod", map[string]string{"test1": "value"}, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
+ testKey := "a"
+ klog.ErrorS(nil, "Starting container in a pod", testKey, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
+ klog.InfoS("test: %s", "testname") // want `structured logging function "InfoS" should not use format specifier "%s"`
+ klog.ErrorS(nil, "test no.: %d", 1) // want `structured logging function "ErrorS" should not use format specifier "%d"`
+
+ // Unstructured logs
+ // Error is not expected as this package allows unstructured logging
+ klog.V(1).Infof("test log")
+ klog.Infof("test log")
+ klog.Info("test log")
+ klog.Infoln("test log")
+ klog.InfoDepth(1, "test log")
+ klog.Warning("test log")
+ klog.Warningf("test log")
+ klog.WarningDepth(1, "test log")
+ klog.Error("test log")
+ klog.Errorf("test log")
+ klog.Errorln("test log")
+ klog.ErrorDepth(1, "test log")
+ klog.Fatal("test log")
+ klog.Fatalf("test log")
+ klog.Fatalln("test log")
+ klog.FatalDepth(1, "test log")
+}
diff --git a/hack/tools/logcheck/testdata/data.go b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
similarity index 88%
rename from hack/tools/logcheck/testdata/data.go
rename to hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
index dcdd0d1ff..19c254abd 100644
--- a/hack/tools/logcheck/testdata/data.go
+++ b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
@@ -14,15 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-// test file for unstructured logging static check tool unit tests.
+// This fake package is created as golang.org/x/tools/go/analysis/analysistest
+// expects it to be here for loading. This package is used to test default
+// behavior which is to report errors when unstructured logging is used.
+// This is a test file for unstructured logging static check tool unit tests.
-package testdata
+package doNotAllowUnstructuredLogs
import (
klog "k8s.io/klog/v2"
)
-func printUnstructuredLog() {
+func doNotAllowUnstructuredLogs() {
+ // Structured logs
+ // Error is expected if structured logging pattern is not used correctly
+ klog.InfoS("test log")
+ klog.ErrorS(nil, "test log")
+ klog.InfoS("Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to InfoS should always be Key Value pairs. Please check if there is any key or value missing.`
+ klog.ErrorS(nil, "Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to ErrorS should always be Key Value pairs. Please check if there is any key or value missing.`
+ klog.InfoS("Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
+ klog.ErrorS(nil, "Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
+ klog.InfoS("Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
+ klog.ErrorS(nil, "Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
+ klog.InfoS("Starting container in a pod", map[string]string{"test1": "value"}, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
+ testKey := "a"
+ klog.ErrorS(nil, "Starting container in a pod", testKey, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
+ klog.InfoS("test: %s", "testname") // want `structured logging function "InfoS" should not use format specifier "%s"`
+ klog.ErrorS(nil, "test no.: %d", 1) // want `structured logging function "ErrorS" should not use format specifier "%d"`
+
+ // Unstructured logs
+ // Error is expected as this package does not allow unstructured logging
klog.V(1).Infof("test log") // want `unstructured logging function "Infof" should not be used`
klog.Infof("test log") // want `unstructured logging function "Infof" should not be used`
klog.Info("test log") // want `unstructured logging function "Info" should not be used`
@@ -41,19 +62,3 @@ func printUnstructuredLog() {
klog.FatalDepth(1, "test log") // want `unstructured logging function "FatalDepth" should not be used`
}
-
-func printStructuredLog() {
- klog.InfoS("test log")
- klog.ErrorS(nil, "test log")
- klog.InfoS("Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to InfoS should always be Key Value pairs. Please check if there is any key or value missing.`
- klog.ErrorS(nil, "Starting container in a pod", "containerID", "containerID", "pod") // want `Additional arguments to ErrorS should always be Key Value pairs. Please check if there is any key or value missing.`
- klog.InfoS("Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
- klog.ErrorS(nil, "Starting container in a pod", "测试", "containerID") // want `Key positional arguments "测试" are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.`
- klog.InfoS("Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
- klog.ErrorS(nil, "Starting container in a pod", 7, "containerID") // want `Key positional arguments are expected to be inlined constant strings. Please replace 7 provided with string value`
- klog.InfoS("Starting container in a pod", map[string]string{"test1": "value"}, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
- testKey := "a"
- klog.ErrorS(nil, "Starting container in a pod", testKey, "containerID") // want `Key positional arguments are expected to be inlined constant strings. `
- klog.InfoS("test: %s", "testname") // want `structured logging function "InfoS" should not use format specifier "%s"`
- klog.ErrorS(nil, "test no.: %d", 1) // want `structured logging function "ErrorS" should not use format specifier "%d"`
-}
From 7cbd2fc97d1ab7abcb24235cfd2d297073ff536f Mon Sep 17 00:00:00 2001
From: yuzhiquan
Date: Thu, 1 Jul 2021 17:46:48 +0800
Subject: [PATCH 005/125] add KObjs function to handle slice of KMetada
---
klog.go | 17 ++++++++++++
klog_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+)
diff --git a/klog.go b/klog.go
index 1e187f763..d24087def 100644
--- a/klog.go
+++ b/klog.go
@@ -1603,3 +1603,20 @@ func KRef(namespace, name string) ObjectRef {
Namespace: namespace,
}
}
+
+// KObjs returns slice of ObjectRef from an slice of ObjectMeta
+func KObjs(arg interface{}) []ObjectRef {
+ s := reflect.ValueOf(arg)
+ if s.Kind() != reflect.Slice {
+ return nil
+ }
+ objectRefs := make([]ObjectRef, 0, s.Len())
+ for i := 0; i < s.Len(); i++ {
+ if v, ok := s.Index(i).Interface().(KMetadata); ok {
+ objectRefs = append(objectRefs, KObj(v))
+ } else {
+ return nil
+ }
+ }
+ return objectRefs
+}
diff --git a/klog_test.go b/klog_test.go
index cd67e4c07..7f6a6471b 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1754,3 +1754,81 @@ func TestKlogFlagPrefix(t *testing.T) {
}
})
}
+
+func TestKObjs(t *testing.T) {
+ tests := []struct {
+ name string
+ obj interface{}
+ want []ObjectRef
+ }{
+ {
+ name: "test for KObjs function with KMetadata slice",
+ obj: []kMetadataMock{
+ {
+ name: "kube-dns",
+ ns: "kube-system",
+ },
+ {
+ name: "mi-conf",
+ },
+ {},
+ },
+ want: []ObjectRef{
+ {
+ Name: "kube-dns",
+ Namespace: "kube-system",
+ },
+ {
+ Name: "mi-conf",
+ },
+ {},
+ },
+ },
+ {
+ name: "test for KObjs function with KMetadata pointer slice",
+ obj: []*kMetadataMock{
+ {
+ name: "kube-dns",
+ ns: "kube-system",
+ },
+ {
+ name: "mi-conf",
+ },
+ nil,
+ },
+ want: []ObjectRef{
+ {
+ Name: "kube-dns",
+ Namespace: "kube-system",
+ },
+ {
+ Name: "mi-conf",
+ },
+ {},
+ },
+ },
+ {
+ name: "test for KObjs function with slice does not implement KMetadata",
+ obj: []int{1, 2, 3, 4, 6},
+ want: nil,
+ },
+ {
+ name: "test for KObjs function with interface",
+ obj: "test case",
+ want: nil,
+ },
+ {
+ name: "test for KObjs function with nil",
+ obj: nil,
+ want: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !reflect.DeepEqual(KObjs(tt.obj), tt.want) {
+ t.Errorf("\nwant:\t %v\n got:\t %v", tt.want, KObjs(tt.obj))
+ }
+ })
+ }
+}
From 9ab3c2b56cb25ed5aacb570d25894a6f9b302e93 Mon Sep 17 00:00:00 2001
From: yuzhiquan
Date: Mon, 5 Jul 2021 10:26:15 +0800
Subject: [PATCH 006/125] add serathius as approvers of klog
---
OWNERS | 1 +
1 file changed, 1 insertion(+)
diff --git a/OWNERS b/OWNERS
index f689c1641..ad5063fdf 100644
--- a/OWNERS
+++ b/OWNERS
@@ -16,3 +16,4 @@ approvers:
- piosz
- brancz
- lavalamp
+ - serathius
From 4171f3c1be1b60901704dfbd140b9f03161427b1 Mon Sep 17 00:00:00 2001
From: Davanum Srinivas
Date: Sat, 29 May 2021 22:41:17 -0400
Subject: [PATCH 007/125] Switching to logr tag v1.0.0
Signed-off-by: Davanum Srinivas
---
go.mod | 2 +-
go.sum | 6 +-
klog.go | 66 +++++-----
klog_test.go | 54 ++++----
.../calldepth-test/call_depth_helper_test.go | 4 +-
klogr/klogr.go | 115 ++++++------------
klogr/klogr_test.go | 10 +-
7 files changed, 117 insertions(+), 140 deletions(-)
diff --git a/go.mod b/go.mod
index eb297b6a1..08a2d0f31 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module k8s.io/klog/v2
go 1.13
-require github.com/go-logr/logr v0.4.0
+require github.com/go-logr/logr v1.0.0
diff --git a/go.sum b/go.sum
index 5778f8174..a1b90e4bc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
-github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
-github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.0.0-rc1 h1:+ul9F74rBkPajeP8m4o3o0tiglmzNFsPnuhYyBCQ0Sc=
+github.com/go-logr/logr v1.0.0-rc1/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.0.0 h1:kH951GinvFVaQgy/ki/B3YYmQtRpExGigSJg6O8z5jo=
+github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
diff --git a/klog.go b/klog.go
index d24087def..d6f52bd43 100644
--- a/klog.go
+++ b/klog.go
@@ -509,7 +509,7 @@ type loggingT struct {
addDirHeader bool
// If set, all output will be redirected unconditionally to the provided logr.Logger
- logr logr.Logger
+ logr *logr.Logger
// If true, messages will not be propagated to lower severity log levels
oneOutput bool
@@ -698,11 +698,11 @@ func (buf *buffer) someDigits(i, d int) int {
return copy(buf.tmp[i:], buf.tmp[j:])
}
-func (l *loggingT) println(s severity, logr logr.Logger, filter LogFilter, args ...interface{}) {
+func (l *loggingT) println(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
buf, file, line := l.header(s, 0)
- // if logr is set, we clear the generated header as we rely on the backing
- // logr implementation to print headers
- if logr != nil {
+ // if logger is set, we clear the generated header as we rely on the backing
+ // logger implementation to print headers
+ if logger != nil {
l.putBuffer(buf)
buf = l.getBuffer()
}
@@ -710,18 +710,18 @@ func (l *loggingT) println(s severity, logr logr.Logger, filter LogFilter, args
args = filter.Filter(args)
}
fmt.Fprintln(buf, args...)
- l.output(s, logr, buf, 0 /* depth */, file, line, false)
+ l.output(s, logger, buf, 0 /* depth */, file, line, false)
}
-func (l *loggingT) print(s severity, logr logr.Logger, filter LogFilter, args ...interface{}) {
- l.printDepth(s, logr, filter, 1, args...)
+func (l *loggingT) print(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
+ l.printDepth(s, logger, filter, 1, args...)
}
-func (l *loggingT) printDepth(s severity, logr logr.Logger, filter LogFilter, depth int, args ...interface{}) {
+func (l *loggingT) printDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
buf, file, line := l.header(s, depth)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
- if logr != nil {
+ if logger != nil {
l.putBuffer(buf)
buf = l.getBuffer()
}
@@ -732,14 +732,14 @@ func (l *loggingT) printDepth(s severity, logr logr.Logger, filter LogFilter, de
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
- l.output(s, logr, buf, depth, file, line, false)
+ l.output(s, logger, buf, depth, file, line, false)
}
-func (l *loggingT) printf(s severity, logr logr.Logger, filter LogFilter, format string, args ...interface{}) {
+func (l *loggingT) printf(s severity, logger *logr.Logger, filter LogFilter, format string, args ...interface{}) {
buf, file, line := l.header(s, 0)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
- if logr != nil {
+ if logger != nil {
l.putBuffer(buf)
buf = l.getBuffer()
}
@@ -750,17 +750,17 @@ func (l *loggingT) printf(s severity, logr logr.Logger, filter LogFilter, format
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
- l.output(s, logr, buf, 0 /* depth */, file, line, false)
+ l.output(s, logger, buf, 0 /* depth */, file, line, false)
}
// printWithFileLine behaves like print but uses the provided file and line number. If
// alsoLogToStderr is true, the log message always appears on standard error; it
// will also appear in the log file unless --logtostderr is set.
-func (l *loggingT) printWithFileLine(s severity, logr logr.Logger, filter LogFilter, file string, line int, alsoToStderr bool, args ...interface{}) {
+func (l *loggingT) printWithFileLine(s severity, logger *logr.Logger, filter LogFilter, file string, line int, alsoToStderr bool, args ...interface{}) {
buf := l.formatHeader(s, file, line)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
- if logr != nil {
+ if logger != nil {
l.putBuffer(buf)
buf = l.getBuffer()
}
@@ -771,28 +771,28 @@ func (l *loggingT) printWithFileLine(s severity, logr logr.Logger, filter LogFil
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
- l.output(s, logr, buf, 2 /* depth */, file, line, alsoToStderr)
+ l.output(s, logger, buf, 2 /* depth */, file, line, alsoToStderr)
}
// if loggr is specified, will call loggr.Error, otherwise output with logging module.
-func (l *loggingT) errorS(err error, loggr logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) {
+func (l *loggingT) errorS(err error, logger *logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) {
if filter != nil {
msg, keysAndValues = filter.FilterS(msg, keysAndValues)
}
- if loggr != nil {
- logr.WithCallDepth(loggr, depth+2).Error(err, msg, keysAndValues...)
+ if logger != nil {
+ logger.WithCallDepth(depth+2).Error(err, msg, keysAndValues...)
return
}
l.printS(err, errorLog, depth+1, msg, keysAndValues...)
}
// if loggr is specified, will call loggr.Info, otherwise output with logging module.
-func (l *loggingT) infoS(loggr logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) {
+func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) {
if filter != nil {
msg, keysAndValues = filter.FilterS(msg, keysAndValues)
}
- if loggr != nil {
- logr.WithCallDepth(loggr, depth+2).Info(msg, keysAndValues...)
+ if logger != nil {
+ logger.WithCallDepth(depth+2).Info(msg, keysAndValues...)
return
}
l.printS(nil, infoLog, depth+1, msg, keysAndValues...)
@@ -866,7 +866,14 @@ func SetLogger(logr logr.Logger) {
logging.mu.Lock()
defer logging.mu.Unlock()
- logging.logr = logr
+ logging.logr = &logr
+}
+
+func clearLogger() {
+ logging.mu.Lock()
+ defer logging.mu.Unlock()
+
+ logging.logr = nil
}
// SetOutput sets the output destination for all severities
@@ -904,7 +911,7 @@ func LogToStderr(stderr bool) {
}
// output writes the data to the log files and releases the buffer.
-func (l *loggingT) output(s severity, log logr.Logger, buf *buffer, depth int, file string, line int, alsoToStderr bool) {
+func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int, file string, line int, alsoToStderr bool) {
l.mu.Lock()
if l.traceLocation.isSet() {
if l.traceLocation.match(file, line) {
@@ -916,9 +923,9 @@ func (l *loggingT) output(s severity, log logr.Logger, buf *buffer, depth int, f
// TODO: set 'severity' and caller information as structured log info
// keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line}
if s == errorLog {
- logr.WithCallDepth(l.logr, depth+3).Error(nil, string(data))
+ l.logr.WithCallDepth(depth+3).Error(nil, string(data))
} else {
- logr.WithCallDepth(log, depth+3).Info(string(data))
+ log.WithCallDepth(depth + 3).Info(string(data))
}
} else if l.toStderr {
os.Stderr.Write(data)
@@ -1269,7 +1276,7 @@ func (l *loggingT) setV(pc uintptr) Level {
// See the documentation of V for more information.
type Verbose struct {
enabled bool
- logr logr.Logger
+ logr *logr.Logger
filter LogFilter
}
@@ -1277,7 +1284,8 @@ func newVerbose(level Level, b bool) Verbose {
if logging.logr == nil {
return Verbose{b, nil, logging.filter}
}
- return Verbose{b, logging.logr.V(int(level)), logging.filter}
+ v := logging.logr.V(int(level))
+ return Verbose{b, &v, logging.filter}
}
// V reports whether verbosity at the call site is at least the requested level.
diff --git a/klog_test.go b/klog_test.go
index 7f6a6471b..0df1420f2 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -446,7 +446,7 @@ func testVmoduleGlob(pat string, match bool, t *testing.T) {
defer logging.vmodule.Set("")
logging.vmodule.Set(pat)
if V(2).Enabled() != match {
- t.Errorf("incorrect match for %q: got %t expected %t", pat, V(2), match)
+ t.Errorf("incorrect match for %q: got %#v expected %#v", pat, V(2), match)
}
}
@@ -1375,8 +1375,9 @@ func TestInfoSWithLogr(t *testing.T) {
for _, data := range testDataInfo {
t.Run(data.msg, func(t *testing.T) {
- SetLogger(logger)
- defer SetLogger(nil)
+ l := logr.New(logger)
+ SetLogger(l)
+ defer clearLogger()
defer logger.reset()
InfoS(data.msg, data.keysValues...)
@@ -1442,8 +1443,9 @@ func TestErrorSWithLogr(t *testing.T) {
for _, data := range testDataInfo {
t.Run(data.msg, func(t *testing.T) {
- SetLogger(logger)
- defer SetLogger(nil)
+ l := logr.New(logger)
+ SetLogger(l)
+ defer clearLogger()
defer logger.reset()
ErrorS(data.err, data.msg, data.keysValues...)
@@ -1499,8 +1501,9 @@ func TestCallDepthLogr(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- SetLogger(logger)
- defer SetLogger(nil)
+ l := logr.New(logger)
+ SetLogger(l)
+ defer clearLogger()
defer logger.reset()
defer logger.resetCallDepth()
@@ -1520,7 +1523,8 @@ func TestCallDepthLogr(t *testing.T) {
func TestCallDepthLogrInfoS(t *testing.T) {
logger := &callDepthTestLogr{}
logger.resetCallDepth()
- SetLogger(logger)
+ l := logr.New(logger)
+ SetLogger(l)
// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
logFunc := func() {
@@ -1541,7 +1545,8 @@ func TestCallDepthLogrInfoS(t *testing.T) {
func TestCallDepthLogrErrorS(t *testing.T) {
logger := &callDepthTestLogr{}
logger.resetCallDepth()
- SetLogger(logger)
+ l := logr.New(logger)
+ SetLogger(l)
// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
logFunc := func() {
@@ -1562,7 +1567,8 @@ func TestCallDepthLogrErrorS(t *testing.T) {
func TestCallDepthLogrGoLog(t *testing.T) {
logger := &callDepthTestLogr{}
logger.resetCallDepth()
- SetLogger(logger)
+ l := logr.New(logger)
+ SetLogger(l)
CopyStandardLogTo("INFO")
// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
@@ -1588,7 +1594,7 @@ func TestCallDepthTestLogr(t *testing.T) {
logger.resetCallDepth()
logFunc := func() {
- logger.Info("some info log")
+ logger.Info(0, "some info log")
}
// Keep these lines together.
_, wantFile, wantLine, _ := runtime.Caller(0)
@@ -1634,7 +1640,7 @@ func (l *testLogr) reset() {
l.entries = []testLogrEntry{}
}
-func (l *testLogr) Info(msg string, keysAndValues ...interface{}) {
+func (l *testLogr) Info(level int, msg string, keysAndValues ...interface{}) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.entries = append(l.entries, testLogrEntry{
@@ -1655,12 +1661,15 @@ func (l *testLogr) Error(err error, msg string, keysAndValues ...interface{}) {
})
}
-func (l *testLogr) Enabled() bool { panic("not implemented") }
-func (l *testLogr) V(int) logr.Logger { panic("not implemented") }
-func (l *testLogr) WithName(string) logr.Logger { panic("not implemented") }
-func (l *testLogr) WithValues(...interface{}) logr.Logger {
- panic("not implemented")
-}
+func (l *testLogr) Init(info logr.RuntimeInfo) {}
+func (l *testLogr) Enabled(level int) bool { return true }
+func (l *testLogr) V(int) logr.Logger { panic("not implemented") }
+func (l *testLogr) WithName(string) logr.LogSink { panic("not implemented") }
+func (l *testLogr) WithValues(...interface{}) logr.LogSink { panic("not implemented") }
+func (l *testLogr) WithCallDepth(depth int) logr.LogSink { return l }
+
+var _ logr.LogSink = &testLogr{}
+var _ logr.CallDepthLogSink = &testLogr{}
type callDepthTestLogr struct {
testLogr
@@ -1673,17 +1682,17 @@ func (l *callDepthTestLogr) resetCallDepth() {
l.callDepth = 0
}
-func (l *callDepthTestLogr) WithCallDepth(depth int) logr.Logger {
+func (l *callDepthTestLogr) WithCallDepth(depth int) logr.LogSink {
l.mutex.Lock()
defer l.mutex.Unlock()
// Note: Usually WithCallDepth would be implemented by cloning l
// and setting the call depth on the clone. We modify l instead in
// this test helper for simplicity.
- l.callDepth = depth
+ l.callDepth = depth + 1
return l
}
-func (l *callDepthTestLogr) Info(msg string, keysAndValues ...interface{}) {
+func (l *callDepthTestLogr) Info(level int, msg string, keysAndValues ...interface{}) {
l.mutex.Lock()
defer l.mutex.Unlock()
// Add 2 to depth for the wrapper function caller and for invocation in
@@ -1710,6 +1719,9 @@ func (l *callDepthTestLogr) Error(err error, msg string, keysAndValues ...interf
})
}
+var _ logr.LogSink = &callDepthTestLogr{}
+var _ logr.CallDepthLogSink = &callDepthTestLogr{}
+
func checkLogrEntryCorrectCaller(t *testing.T, wantFile string, wantLine int, entry testLogrEntry) {
t.Helper()
diff --git a/klogr/calldepth-test/call_depth_helper_test.go b/klogr/calldepth-test/call_depth_helper_test.go
index be605fb73..03326bf5a 100644
--- a/klogr/calldepth-test/call_depth_helper_test.go
+++ b/klogr/calldepth-test/call_depth_helper_test.go
@@ -8,9 +8,9 @@ import (
// their source code file is *not* logged because of WithCallDepth(1).
func myInfo(l logr.Logger, msg string) {
- logr.WithCallDepth(l, 1).Info(msg)
+ l.WithCallDepth(2).Info(msg)
}
func myInfo2(l logr.Logger, msg string) {
- myInfo(logr.WithCallDepth(l, 1), msg)
+ myInfo(l.WithCallDepth(2), msg)
}
diff --git a/klogr/klogr.go b/klogr/klogr.go
index 4cf80dd1e..a347a8849 100644
--- a/klogr/klogr.go
+++ b/klogr/klogr.go
@@ -6,12 +6,10 @@ import (
"bytes"
"encoding/json"
"fmt"
- "runtime"
- "sort"
- "strings"
-
"github.com/go-logr/logr"
"k8s.io/klog/v2"
+ "sort"
+ "strings"
)
// Option is a functional option that reconfigures the logger created with New.
@@ -57,7 +55,7 @@ func NewWithOptions(options ...Option) logr.Logger {
for _, option := range options {
option(&l)
}
- return l
+ return logr.New(&l)
}
type klogger struct {
@@ -68,40 +66,8 @@ type klogger struct {
format Format
}
-func (l klogger) clone() klogger {
- return klogger{
- level: l.level,
- prefix: l.prefix,
- values: copySlice(l.values),
- format: l.format,
- }
-}
-
-func copySlice(in []interface{}) []interface{} {
- out := make([]interface{}, len(in))
- copy(out, in)
- return out
-}
-
-// Magic string for intermediate frames that we should ignore.
-const autogeneratedFrameName = ""
-
-// Discover how many frames we need to climb to find the caller. This approach
-// was suggested by Ian Lance Taylor of the Go team, so it *should* be safe
-// enough (famous last words).
-//
-// It is needed because binding the specific klogger functions to the
-// logr interface creates one additional call frame that neither we nor
-// our caller know about.
-func framesToCaller() int {
- // 1 is the immediate caller. 3 should be too many.
- for i := 1; i < 3; i++ {
- _, file, _, _ := runtime.Caller(i + 1) // +1 for this function's frame
- if file != autogeneratedFrameName {
- return i
- }
- }
- return 1 // something went wrong, this is safe
+func (l *klogger) Init(info logr.RuntimeInfo) {
+ l.callDepth += info.CallDepth
}
// trimDuplicates will deduplicate elements provided in multiple KV tuple
@@ -191,27 +157,25 @@ func pretty(value interface{}) string {
return strings.TrimSpace(string(buffer.Bytes()))
}
-func (l klogger) Info(msg string, kvList ...interface{}) {
- if l.Enabled() {
- switch l.format {
- case FormatSerialize:
- msgStr := flatten("msg", msg)
- trimmed := trimDuplicates(l.values, kvList)
- fixedStr := flatten(trimmed[0]...)
- userStr := flatten(trimmed[1]...)
- klog.InfoDepth(framesToCaller()+l.callDepth, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr)
- case FormatKlog:
- trimmed := trimDuplicates(l.values, kvList)
- if l.prefix != "" {
- msg = l.prefix + ": " + msg
- }
- klog.InfoSDepth(framesToCaller()+l.callDepth, msg, append(trimmed[0], trimmed[1]...)...)
+func (l klogger) Info(level int, msg string, kvList ...interface{}) {
+ switch l.format {
+ case FormatSerialize:
+ msgStr := flatten("msg", msg)
+ trimmed := trimDuplicates(l.values, kvList)
+ fixedStr := flatten(trimmed[0]...)
+ userStr := flatten(trimmed[1]...)
+ klog.InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr)
+ case FormatKlog:
+ trimmed := trimDuplicates(l.values, kvList)
+ if l.prefix != "" {
+ msg = l.prefix + ": " + msg
}
+ klog.InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...)
}
}
-func (l klogger) Enabled() bool {
- return bool(klog.V(klog.Level(l.level)).Enabled())
+func (l klogger) Enabled(level int) bool {
+ return klog.V(klog.Level(level)).Enabled()
}
func (l klogger) Error(err error, msg string, kvList ...interface{}) {
@@ -226,45 +190,38 @@ func (l klogger) Error(err error, msg string, kvList ...interface{}) {
trimmed := trimDuplicates(l.values, kvList)
fixedStr := flatten(trimmed[0]...)
userStr := flatten(trimmed[1]...)
- klog.ErrorDepth(framesToCaller()+l.callDepth, l.prefix, " ", msgStr, " ", errStr, " ", fixedStr, " ", userStr)
+ klog.ErrorDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", errStr, " ", fixedStr, " ", userStr)
case FormatKlog:
trimmed := trimDuplicates(l.values, kvList)
if l.prefix != "" {
msg = l.prefix + ": " + msg
}
- klog.ErrorSDepth(framesToCaller()+l.callDepth, err, msg, append(trimmed[0], trimmed[1]...)...)
+ klog.ErrorSDepth(l.callDepth+1, err, msg, append(trimmed[0], trimmed[1]...)...)
}
}
-func (l klogger) V(level int) logr.Logger {
- new := l.clone()
- new.level = level
- return new
-}
-
// WithName returns a new logr.Logger with the specified name appended. klogr
// uses '/' characters to separate name elements. Callers should not pass '/'
// in the provided name string, but this library does not actually enforce that.
-func (l klogger) WithName(name string) logr.Logger {
- new := l.clone()
+func (l klogger) WithName(name string) logr.LogSink {
if len(l.prefix) > 0 {
- new.prefix = l.prefix + "/"
+ l.prefix = l.prefix + "/"
}
- new.prefix += name
- return new
+ l.prefix += name
+ return &l
}
-func (l klogger) WithValues(kvList ...interface{}) logr.Logger {
- new := l.clone()
- new.values = append(new.values, kvList...)
- return new
+func (l klogger) WithValues(kvList ...interface{}) logr.LogSink {
+ // Three slice args forces a copy.
+ n := len(l.values)
+ l.values = append(l.values[:n:n], kvList...)
+ return &l
}
-func (l klogger) WithCallDepth(depth int) logr.Logger {
- new := l.clone()
- new.callDepth += depth
- return new
+func (l klogger) WithCallDepth(depth int) logr.LogSink {
+ l.callDepth += depth
+ return &l
}
-var _ logr.Logger = klogger{}
-var _ logr.CallDepthLogger = klogger{}
+var _ logr.LogSink = &klogger{}
+var _ logr.CallDepthLogSink = &klogger{}
diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go
index cb86cc30c..7775b436e 100644
--- a/klogr/klogr_test.go
+++ b/klogr/klogr_test.go
@@ -110,6 +110,7 @@ func testOutput(t *testing.T, format string) {
`,
},
"should correctly handle odd-numbers of KVs": {
+ klogr: new(),
text: "test",
keysAndValues: []interface{}{"akey", "avalue", "akey2"},
expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"=null
@@ -118,6 +119,7 @@ func testOutput(t *testing.T, format string) {
`,
},
"should correctly html characters": {
+ klogr: new(),
text: "test",
keysAndValues: []interface{}{"akey", "<&>"},
expectedOutput: ` "msg"="test" "akey"="<&>"
@@ -169,19 +171,15 @@ func testOutput(t *testing.T, format string) {
}
for n, test := range tests {
t.Run(n, func(t *testing.T) {
- klogr := test.klogr
- if klogr == nil {
- klogr = new()
- }
// hijack the klog output
tmpWriteBuffer := bytes.NewBuffer(nil)
klog.SetOutput(tmpWriteBuffer)
if test.err != nil {
- klogr.Error(test.err, test.text, test.keysAndValues...)
+ test.klogr.Error(test.err, test.text, test.keysAndValues...)
} else {
- klogr.Info(test.text, test.keysAndValues...)
+ test.klogr.Info(test.text, test.keysAndValues...)
}
// call Flush to ensure the text isn't still buffered
From 8ee3d652c96bc3d9ce1075b2350e23fe2a910c76 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 4 Aug 2021 11:40:30 +0200
Subject: [PATCH 008/125] export ClearLogger
Code in Kubernetes also needs the ability to unset a logger.
---
klog.go | 7 ++++++-
klog_test.go | 6 +++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/klog.go b/klog.go
index d6f52bd43..5b33973e5 100644
--- a/klog.go
+++ b/klog.go
@@ -862,6 +862,9 @@ func (rb *redirectBuffer) Write(bytes []byte) (n int, err error) {
// Use as:
// ...
// klog.SetLogger(zapr.NewLogger(zapLog))
+//
+// To remove a backing logr implemention, use ClearLogger. Setting an
+// empty logger with SetLogger(logr.Logger{}) does not work.
func SetLogger(logr logr.Logger) {
logging.mu.Lock()
defer logging.mu.Unlock()
@@ -869,7 +872,9 @@ func SetLogger(logr logr.Logger) {
logging.logr = &logr
}
-func clearLogger() {
+// ClearLogger removes a backing logr implementation if one was set earlier
+// with SetLogger.
+func ClearLogger() {
logging.mu.Lock()
defer logging.mu.Unlock()
diff --git a/klog_test.go b/klog_test.go
index 0df1420f2..9a44c6be3 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1377,7 +1377,7 @@ func TestInfoSWithLogr(t *testing.T) {
t.Run(data.msg, func(t *testing.T) {
l := logr.New(logger)
SetLogger(l)
- defer clearLogger()
+ defer ClearLogger()
defer logger.reset()
InfoS(data.msg, data.keysValues...)
@@ -1445,7 +1445,7 @@ func TestErrorSWithLogr(t *testing.T) {
t.Run(data.msg, func(t *testing.T) {
l := logr.New(logger)
SetLogger(l)
- defer clearLogger()
+ defer ClearLogger()
defer logger.reset()
ErrorS(data.err, data.msg, data.keysValues...)
@@ -1503,7 +1503,7 @@ func TestCallDepthLogr(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
l := logr.New(logger)
SetLogger(l)
- defer clearLogger()
+ defer ClearLogger()
defer logger.reset()
defer logger.resetCallDepth()
From d648c2e42d303b501c07abea92d372bdf649c00d Mon Sep 17 00:00:00 2001
From: Austin Clements
Date: Mon, 11 Jan 2021 17:46:00 -0500
Subject: [PATCH 009/125] fix file-based filtering symbolization
If file-filtered logging is enabled, V looks up its caller's source
file path using runtime.Callers and runtime.FuncForPC. However,
runtime.Callers returns return PCs, which aren't necessarily in the
same source file as the call site. For example, if F calls V, and F's
call to V is immediately followed by an inlined called to another
function, say G, then symbolizing V's return PC will actually return
G's file and line number, not F's.
Fix this by subtracting 1 from the PC returned by runtime.Callers to
back up from the return PC to the call PC.
An arguably better fix would be to use runtime.CallersFrames, which
abstracts away such details. Unfortunately, CallersFrames allocates
(at least as of Go 1.15), so we avoid it here.
---
klog.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/klog.go b/klog.go
index 5b33973e5..60c88d3c3 100644
--- a/klog.go
+++ b/klog.go
@@ -1328,9 +1328,14 @@ func V(level Level) Verbose {
if runtime.Callers(2, logging.pcs[:]) == 0 {
return newVerbose(level, false)
}
- v, ok := logging.vmap[logging.pcs[0]]
+ // runtime.Callers returns "return PCs", but we want
+ // to look up the symbolic information for the call,
+ // so subtract 1 from the PC. runtime.CallersFrames
+ // would be cleaner, but allocates.
+ pc := logging.pcs[0] - 1
+ v, ok := logging.vmap[pc]
if !ok {
- v = logging.setV(logging.pcs[0])
+ v = logging.setV(pc)
}
return newVerbose(level, v >= level)
}
From 83653a6deebf1ec56fad874e3c7826f5e5f18197 Mon Sep 17 00:00:00 2001
From: Davanum Srinivas
Date: Wed, 1 Sep 2021 20:53:26 -0700
Subject: [PATCH 010/125] Update to newest versions of golang 1.17.x
Signed-off-by: Davanum Srinivas
---
.github/workflows/test.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2aa4fb29a..1e6806d1c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
- go-versions: [1.14.x, 1.15.x, 1.16.x]
+ go-versions: [1.14.x, 1.15.x, 1.16.x, 1.17.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
From f9de652e75efc2160e33cd9435883ae6ce670cdb Mon Sep 17 00:00:00 2001
From: luyou86 <205893776@qq.com>
Date: Fri, 17 Sep 2021 00:18:16 +0800
Subject: [PATCH 011/125] Fix logcheck exit function
---
hack/tools/logcheck/main.go | 1 +
.../allowUnstructuredLogs.go | 4 ++++
.../doNotAllowUnstructuredLogs.go | 5 ++++-
.../testdata/src/k8s.io/klog/v2/klog.go | 17 +++++++++++++++++
4 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/hack/tools/logcheck/main.go b/hack/tools/logcheck/main.go
index f82f6ea0d..ffafecdbd 100644
--- a/hack/tools/logcheck/main.go
+++ b/hack/tools/logcheck/main.go
@@ -129,6 +129,7 @@ func isUnstructured(fName string) bool {
"Warning", "Warningf", "Warningln", "WarningDepth",
"Error", "Errorf", "Errorln", "ErrorDepth",
"Fatal", "Fatalf", "Fatalln", "FatalDepth",
+ "Exit", "Exitf", "Exitln", "ExitDepth",
}
for _, name := range unstrucured {
diff --git a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
index 346410d52..879e5cf56 100644
--- a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
+++ b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go
@@ -60,4 +60,8 @@ func allowUnstructuredLogs() {
klog.Fatalf("test log")
klog.Fatalln("test log")
klog.FatalDepth(1, "test log")
+ klog.Exit("test log")
+ klog.ExitDepth(1, "test log")
+ klog.Exitln("test log")
+ klog.Exitf("test log")
}
diff --git a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
index 19c254abd..f4a6eebc3 100644
--- a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
+++ b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
@@ -60,5 +60,8 @@ func doNotAllowUnstructuredLogs() {
klog.Fatalf("test log") // want `unstructured logging function "Fatalf" should not be used`
klog.Fatalln("test log") // want `unstructured logging function "Fatalln" should not be used`
klog.FatalDepth(1, "test log") // want `unstructured logging function "FatalDepth" should not be used`
-
+ klog.Exit("test log") // want `unstructured logging function "Exit" should not be used`
+ klog.Exitf("test log") // want `unstructured logging function "Exitf" should not be used`
+ klog.Exitln("test log") // want `unstructured logging function "Exitln" should not be used`
+ klog.ExitDepth(1, "test log") // want `unstructured logging function "ExitDepth" should not be used`
}
diff --git a/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go b/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go
index 63bfafece..c43ee166b 100644
--- a/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go
+++ b/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go
@@ -180,3 +180,20 @@ func Fatalln(args ...interface{}) {
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Fatalf(format string, args ...interface{}) {
}
+
+func Exit(args ...interface{}) {
+}
+
+// ExitDepth acts as Exit but uses depth to determine which call frame to log.
+// ExitDepth(0, "msg") is the same as Exit("msg").
+func ExitDepth(depth int, args ...interface{}) {
+}
+
+// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
+func Exitln(args ...interface{}) {
+}
+
+// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
+// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
+func Exitf(format string, args ...interface{}) {
+}
From f35102364db86214b5b0fc6610fb3add6850afef Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Mon, 13 Sep 2021 09:54:27 +0200
Subject: [PATCH 012/125] custom marshaler for ObjectRef
This will be used by zapr for the JSON output format in Kubernetes. Without it,
the String method will be used, which is not as intended.
---
go.mod | 2 +-
go.sum | 6 ++----
klog.go | 9 +++++++++
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/go.mod b/go.mod
index 08a2d0f31..31aefba74 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module k8s.io/klog/v2
go 1.13
-require github.com/go-logr/logr v1.0.0
+require github.com/go-logr/logr v1.2.0
diff --git a/go.sum b/go.sum
index a1b90e4bc..919fbadbc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,2 @@
-github.com/go-logr/logr v1.0.0-rc1 h1:+ul9F74rBkPajeP8m4o3o0tiglmzNFsPnuhYyBCQ0Sc=
-github.com/go-logr/logr v1.0.0-rc1/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
-github.com/go-logr/logr v1.0.0 h1:kH951GinvFVaQgy/ki/B3YYmQtRpExGigSJg6O8z5jo=
-github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
diff --git a/klog.go b/klog.go
index 60c88d3c3..45efbb075 100644
--- a/klog.go
+++ b/klog.go
@@ -1591,6 +1591,15 @@ func (ref ObjectRef) String() string {
return ref.Name
}
+// MarshalLog ensures that loggers with support for structured output will log
+// as a struct by removing the String method via a custom type.
+func (ref ObjectRef) MarshalLog() interface{} {
+ type or ObjectRef
+ return or(ref)
+}
+
+var _ logr.Marshaler = ObjectRef{}
+
// KMetadata is a subset of the kubernetes k8s.io/apimachinery/pkg/apis/meta/v1.Object interface
// this interface may expand in the future, but will always be a subset of the
// kubernetes k8s.io/apimachinery/pkg/apis/meta/v1.Object interface
From 16e27a903364897551d1f14f9ea620f350f4c3ba Mon Sep 17 00:00:00 2001
From: Jamie Phillips
Date: Wed, 17 Nov 2021 15:12:35 -0500
Subject: [PATCH 013/125] Using OS targeted go files to separate out the
username logic.
We have discovered that in some instances the runtime.GOOS="windows" check is failing on nanoserver when doing cross compilation. It appears to be inconsistent. Moving this into it's own OS targeted go file prevents it from being an issue.
Signed-off-by: Jamie Phillips
---
klog_file.go | 34 ----------------------------------
klog_file_others.go | 19 +++++++++++++++++++
klog_file_windows.go | 34 ++++++++++++++++++++++++++++++++++
3 files changed, 53 insertions(+), 34 deletions(-)
create mode 100644 klog_file_others.go
create mode 100644 klog_file_windows.go
diff --git a/klog_file.go b/klog_file.go
index de830d922..1025d644f 100644
--- a/klog_file.go
+++ b/klog_file.go
@@ -22,9 +22,7 @@ import (
"errors"
"fmt"
"os"
- "os/user"
"path/filepath"
- "runtime"
"strings"
"sync"
"time"
@@ -57,38 +55,6 @@ func init() {
}
}
-func getUserName() string {
- userNameOnce.Do(func() {
- // On Windows, the Go 'user' package requires netapi32.dll.
- // This affects Windows Nano Server:
- // https://github.com/golang/go/issues/21867
- // Fallback to using environment variables.
- if runtime.GOOS == "windows" {
- u := os.Getenv("USERNAME")
- if len(u) == 0 {
- return
- }
- // Sanitize the USERNAME since it may contain filepath separators.
- u = strings.Replace(u, `\`, "_", -1)
-
- // user.Current().Username normally produces something like 'USERDOMAIN\USERNAME'
- d := os.Getenv("USERDOMAIN")
- if len(d) != 0 {
- userName = d + "_" + u
- } else {
- userName = u
- }
- } else {
- current, err := user.Current()
- if err == nil {
- userName = current.Username
- }
- }
- })
-
- return userName
-}
-
// shortHostname returns its argument, truncating at the first period.
// For instance, given "www.google.com" it returns "www".
func shortHostname(hostname string) string {
diff --git a/klog_file_others.go b/klog_file_others.go
new file mode 100644
index 000000000..aa4672685
--- /dev/null
+++ b/klog_file_others.go
@@ -0,0 +1,19 @@
+//go:build !windows
+// +build !windows
+
+package klog
+
+import (
+ "os/user"
+)
+
+func getUserName() string {
+ userNameOnce.Do(func() {
+ current, err := user.Current()
+ if err == nil {
+ userName = current.Username
+ }
+ })
+
+ return userName
+}
diff --git a/klog_file_windows.go b/klog_file_windows.go
new file mode 100644
index 000000000..2517f9c53
--- /dev/null
+++ b/klog_file_windows.go
@@ -0,0 +1,34 @@
+//go:build windows
+// +build windows
+
+package klog
+
+import (
+ "os"
+ "strings"
+)
+
+func getUserName() string {
+ userNameOnce.Do(func() {
+ // On Windows, the Go 'user' package requires netapi32.dll.
+ // This affects Windows Nano Server:
+ // https://github.com/golang/go/issues/21867
+ // Fallback to using environment variables.
+ u := os.Getenv("USERNAME")
+ if len(u) == 0 {
+ return
+ }
+ // Sanitize the USERNAME since it may contain filepath separators.
+ u = strings.Replace(u, `\`, "_", -1)
+
+ // user.Current().Username normally produces something like 'USERDOMAIN\USERNAME'
+ d := os.Getenv("USERDOMAIN")
+ if len(d) != 0 {
+ userName = d + "_" + u
+ } else {
+ userName = u
+ }
+ })
+
+ return userName
+}
From cc9898f000b843862e181121dad7241b99446ec5 Mon Sep 17 00:00:00 2001
From: Cameron Accola
Date: Thu, 18 Nov 2021 22:47:43 -0600
Subject: [PATCH 014/125] **What this PR does / why we need it**: Fixes minor
documentation issue. The reference to 'glog' should be 'klog'.
---
klog.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/klog.go b/klog.go
index 45efbb075..a65a798ef 100644
--- a/klog.go
+++ b/klog.go
@@ -1297,7 +1297,7 @@ func newVerbose(level Level, b bool) Verbose {
// The returned value is a struct of type Verbose, which implements Info, Infoln
// and Infof. These methods will write to the Info log if called.
// Thus, one may write either
-// if glog.V(2).Enabled() { klog.Info("log this") }
+// if klog.V(2).Enabled() { klog.Info("log this") }
// or
// klog.V(2).Info("log this")
// The second form is shorter but the first is cheaper if logging is off because it does
From 5ecf788fcdd2e3e37533a6cbfc07512a597ed2be Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Fri, 12 Nov 2021 13:58:38 +0100
Subject: [PATCH 015/125] examples: use current klog, add structured logging
example
Structured logging with InfoS is formatted differently than traditional output,
so it's worthwhile to have an example that demostrates that.
The new example didn't compile initially because an older version of klog was
used. This gets avoids by always using the source from the current directory.
---
examples/go.mod | 4 +-
examples/go.sum | 2 +
.../structured_logging/structured_logging.go | 74 +++++++++++++++++++
3 files changed, 79 insertions(+), 1 deletion(-)
create mode 100644 examples/structured_logging/structured_logging.go
diff --git a/examples/go.mod b/examples/go.mod
index c348862d3..2768f2c1e 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -4,5 +4,7 @@ go 1.13
require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
- k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc
+ k8s.io/klog/v2 v2.30.0
)
+
+replace k8s.io/klog/v2 => ../
diff --git a/examples/go.sum b/examples/go.sum
index d00155c98..4d41cc903 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,5 +1,7 @@
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc h1:E/enZ+SqXD3ChluFNvXqlLcUkqMQQDpiyGruRq5pjvY=
diff --git a/examples/structured_logging/structured_logging.go b/examples/structured_logging/structured_logging.go
new file mode 100644
index 000000000..6e80bd936
--- /dev/null
+++ b/examples/structured_logging/structured_logging.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "flag"
+
+ "k8s.io/klog/v2"
+)
+
+// MyStruct will be logged via %+v
+type MyStruct struct {
+ Name string
+ Data string
+ internal int
+}
+
+// MyStringer will be logged as string, with String providing that string.
+type MyString MyStruct
+
+func (m MyString) String() string {
+ return m.Name + ": " + m.Data
+}
+
+func main() {
+ klog.InitFlags(nil)
+ flag.Parse()
+
+ someData := MyStruct{
+ Name: "hello",
+ Data: "world",
+ }
+
+ longData := MyStruct{
+ Name: "long",
+ Data: `Multiple
+lines
+with quite a bit
+of text.`,
+ }
+
+ logData := MyStruct{
+ Name: "log output from some program",
+ Data: `I0000 12:00:00.000000 123456 main.go:42] Starting
+E0000 12:00:01.000000 123456 main.go:43] Failed for some reason
+`,
+ }
+
+ stringData := MyString(longData)
+
+ klog.Infof("someData printed using InfoF: %v", someData)
+ klog.Infof("longData printed using InfoF: %v", longData)
+ klog.Infof(`stringData printed using InfoF,
+with the message across multiple lines:
+%v`, stringData)
+ klog.Infof("logData printed using InfoF:\n%v", logData)
+
+ klog.Info("=============================================")
+
+ klog.InfoS("using InfoS", "someData", someData)
+ klog.InfoS("using InfoS", "longData", longData)
+ klog.InfoS(`using InfoS with
+the message across multiple lines`,
+ "int", 1,
+ "stringData", stringData,
+ "str", "another value")
+ klog.InfoS("using InfoS", "logData", logData)
+ klog.InfoS("using InfoS", "boolean", true, "int", 1, "float", 0.1)
+
+ // The Kubernetes recommendation is to start the message with uppercase
+ // and not end with punctuation. See
+ // https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md
+ klog.InfoS("Did something", "item", "foobar")
+ // Not recommended, but also works.
+ klog.InfoS("This is a full sentence.", "item", "foobar")
+}
From f930164a1d50ed24082a149822e6fd8606df6cdb Mon Sep 17 00:00:00 2001
From: astraw99
Date: Fri, 19 Nov 2021 10:32:11 +0800
Subject: [PATCH 016/125] fix logger unlock on panic
---
klog.go | 14 ++++++++++++--
klog_test.go | 32 ++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/klog.go b/klog.go
index 45efbb075..7f4f94aa5 100644
--- a/klog.go
+++ b/klog.go
@@ -917,7 +917,15 @@ func LogToStderr(stderr bool) {
// output writes the data to the log files and releases the buffer.
func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int, file string, line int, alsoToStderr bool) {
+ var isLocked = true
l.mu.Lock()
+ defer func() {
+ if isLocked {
+ // Unlock before returning in case that it wasn't done already.
+ l.mu.Unlock()
+ }
+ }()
+
if l.traceLocation.isSet() {
if l.traceLocation.match(file, line) {
buf.Write(stacks(false))
@@ -980,6 +988,7 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
// If we got here via Exit rather than Fatal, print no stacks.
if atomic.LoadUint32(&fatalNoStacks) > 0 {
l.mu.Unlock()
+ isLocked = false
timeoutFlush(10 * time.Second)
os.Exit(1)
}
@@ -997,11 +1006,12 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
}
}
l.mu.Unlock()
+ isLocked = false
timeoutFlush(10 * time.Second)
- os.Exit(255) // C++ uses -1, which is silly because it's anded with 255 anyway.
+ os.Exit(255) // C++ uses -1, which is silly because it's anded(&) with 255 anyway.
}
l.putBuffer(buf)
- l.mu.Unlock()
+
if stats := severityStats[s]; stats != nil {
atomic.AddInt64(&stats.lines, 1)
atomic.AddInt64(&stats.bytes, int64(len(data)))
diff --git a/klog_test.go b/klog_test.go
index 9a44c6be3..ad70040c6 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1844,3 +1844,35 @@ func TestKObjs(t *testing.T) {
})
}
}
+
+// Benchmark test for lock with/without defer
+type structWithLock struct {
+ m sync.Mutex
+ n int64
+}
+
+func BenchmarkWithoutDeferUnLock(b *testing.B) {
+ s := structWithLock{}
+ for i := 0; i < b.N; i++ {
+ s.addWithoutDefer()
+ }
+}
+
+func BenchmarkWithDeferUnLock(b *testing.B) {
+ s := structWithLock{}
+ for i := 0; i < b.N; i++ {
+ s.addWithDefer()
+ }
+}
+
+func (s *structWithLock) addWithoutDefer() {
+ s.m.Lock()
+ s.n++
+ s.m.Unlock()
+}
+
+func (s *structWithLock) addWithDefer() {
+ s.m.Lock()
+ defer s.m.Unlock()
+ s.n++
+}
From ae76ae01cfcecaa75607a5b7970855842f667933 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Fri, 26 Nov 2021 11:29:02 +0100
Subject: [PATCH 017/125] structured output: reuse buffer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
There is a mechanism for allocating smallish buffers (< 256 bytes) only once
and then reusing them. Doing that for the structured key/value pairs is an
improvement (measured with https://github.com/kubernetes/kubernetes/pull/106594):
$ $GOPATH/bin/benchstat /tmp/original /tmp/reuse-buffer
name old time/op new time/op delta
Logging/container/structured-36 41.6µs ± 1% 41.6µs ± 1% ~ (p=1.000 n=5+5)
Logging/error-value/structured-36 4.01µs ± 3% 3.37µs ± 3% -16.01% (p=0.008 n=5+5)
Logging/error/structured-36 3.91µs ± 1% 3.33µs ± 3% -14.90% (p=0.008 n=5+5)
Logging/simple/structured-36 3.54µs ± 3% 2.98µs ± 3% -15.77% (p=0.008 n=5+5)
Logging/values/structured-36 6.83µs ± 2% 5.31µs ± 5% -22.34% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
Logging/container/structured-36 10.4kB ± 0% 10.5kB ± 0% +0.61% (p=0.008 n=5+5)
Logging/error-value/structured-36 432B ± 0% 320B ± 0% -25.93% (p=0.008 n=5+5)
Logging/error/structured-36 448B ± 0% 336B ± 0% -25.00% (p=0.008 n=5+5)
Logging/simple/structured-36 408B ± 0% 296B ± 0% -27.45% (p=0.008 n=5+5)
Logging/values/structured-36 777B ± 0% 505B ± 0% -35.01% (p=0.008 n=5+5)
name old allocs/op new allocs/op delta
Logging/container/structured-36 64.0 ± 0% 64.0 ± 0% ~ (all equal)
Logging/error-value/structured-36 9.00 ± 0% 7.00 ± 0% -22.22% (p=0.008 n=5+5)
Logging/error/structured-36 10.0 ± 0% 8.0 ± 0% -20.00% (p=0.008 n=5+5)
Logging/simple/structured-36 8.00 ± 0% 6.00 ± 0% -25.00% (p=0.008 n=5+5)
Logging/values/structured-36 16.0 ± 0% 13.0 ± 0% -18.75% (p=0.008 n=5+5)
---
klog.go | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/klog.go b/klog.go
index 45efbb075..329c6c103 100644
--- a/klog.go
+++ b/klog.go
@@ -801,14 +801,17 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s
// printS is called from infoS and errorS if loggr is not specified.
// set log severity by s
func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
- b := &bytes.Buffer{}
+ // Only create a new buffer if we don't have one cached.
+ b := l.getBuffer()
b.WriteString(fmt.Sprintf("%q", msg))
if err != nil {
b.WriteByte(' ')
b.WriteString(fmt.Sprintf("err=%q", err.Error()))
}
- kvListFormat(b, keysAndValues...)
- l.printDepth(s, logging.logr, nil, depth+1, b)
+ kvListFormat(&b.Buffer, keysAndValues...)
+ l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer)
+ // Make the buffer available for reuse.
+ l.putBuffer(b)
}
const missingValue = "(MISSING)"
From aac832f981b9215987d33db11d845e54270fcf08 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Fri, 26 Nov 2021 11:50:38 +0100
Subject: [PATCH 018/125] structured output: reduce usage of Sprintf
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
We can avoid several allocations and use faster code (= strconv.Quote) by
relying less on Sprintf.
$ $GOPATH/bin/benchstat /tmp/reuse-buffer /tmp/strconv
name old time/op new time/op delta
Logging/container/structured-36 41.6µs ± 1% 39.5µs ± 2% -5.05% (p=0.008 n=5+5)
Logging/error-value/structured-36 3.37µs ± 3% 3.13µs ± 0% -7.06% (p=0.016 n=5+4)
Logging/error/structured-36 3.33µs ± 3% 3.09µs ± 2% -7.10% (p=0.008 n=5+5)
Logging/simple/structured-36 2.98µs ± 3% 2.86µs ± 4% ~ (p=0.095 n=5+5)
Logging/values/structured-36 5.31µs ± 5% 4.77µs ± 4% -10.17% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
Logging/container/structured-36 10.5kB ± 0% 9.3kB ± 0% -11.01% (p=0.008 n=5+5)
Logging/error-value/structured-36 320B ± 0% 312B ± 0% -2.50% (p=0.008 n=5+5)
Logging/error/structured-36 336B ± 0% 312B ± 0% -7.14% (p=0.008 n=5+5)
Logging/simple/structured-36 296B ± 0% 288B ± 0% -2.70% (p=0.008 n=5+5)
Logging/values/structured-36 505B ± 0% 464B ± 0% -8.12% (p=0.008 n=5+5)
name old allocs/op new allocs/op delta
Logging/container/structured-36 64.0 ± 0% 62.0 ± 0% -3.12% (p=0.008 n=5+5)
Logging/error-value/structured-36 7.00 ± 0% 6.00 ± 0% -14.29% (p=0.008 n=5+5)
Logging/error/structured-36 8.00 ± 0% 6.00 ± 0% -25.00% (p=0.008 n=5+5)
Logging/simple/structured-36 6.00 ± 0% 5.00 ± 0% -16.67% (p=0.008 n=5+5)
Logging/values/structured-36 13.0 ± 0% 11.0 ± 0% -15.38% (p=0.008 n=5+5)
---
klog.go | 35 +++++++++++++++++++++++------------
1 file changed, 23 insertions(+), 12 deletions(-)
diff --git a/klog.go b/klog.go
index 329c6c103..f8ee06e9f 100644
--- a/klog.go
+++ b/klog.go
@@ -803,10 +803,10 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s
func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
// Only create a new buffer if we don't have one cached.
b := l.getBuffer()
- b.WriteString(fmt.Sprintf("%q", msg))
+ b.WriteString(strconv.Quote(msg))
if err != nil {
- b.WriteByte(' ')
- b.WriteString(fmt.Sprintf("err=%q", err.Error()))
+ b.WriteString(" err=")
+ b.WriteString(strconv.Quote(err.Error()))
}
kvListFormat(&b.Buffer, keysAndValues...)
l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer)
@@ -826,18 +826,29 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
v = missingValue
}
b.WriteByte(' ')
+ if k, ok := k.(string); ok {
+ // Avoid one allocation when the key is a string, which
+ // normally it should be.
+ b.WriteString(k)
+ } else {
+ b.WriteString(fmt.Sprintf("%s", k))
+ }
+ b.WriteByte('=')
- switch v.(type) {
- case string, error:
- b.WriteString(fmt.Sprintf("%s=%q", k, v))
+ switch v := v.(type) {
+ case string:
+ b.WriteString(strconv.Quote(v))
+ case error:
+ b.WriteString(strconv.Quote(v.Error()))
case []byte:
- b.WriteString(fmt.Sprintf("%s=%+q", k, v))
+ // We cannot use the simpler strconv.Quote here
+ // because it does not escape unicode characters, which is
+ // expected by one test!?
+ b.WriteString(fmt.Sprintf("%+q", v))
+ case fmt.Stringer:
+ b.WriteString(strconv.Quote(v.String()))
default:
- if _, ok := v.(fmt.Stringer); ok {
- b.WriteString(fmt.Sprintf("%s=%q", k, v))
- } else {
- b.WriteString(fmt.Sprintf("%s=%+v", k, v))
- }
+ b.WriteString(fmt.Sprintf("%+v", v))
}
}
}
From 6749fe14f4ec9b252dd005986fbea1adb8403f19 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Fri, 26 Nov 2021 12:51:09 +0100
Subject: [PATCH 019/125] structured output: reorder type switch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Checking common types first is slightly faster.
No difference for simple benchmarks:
$ $GOPATH/bin/benchstat /tmp/strconv /tmp/reorder
name old time/op new time/op delta
Logging/container/structured-36 39.5µs ± 2% 39.6µs ± 1% ~ (p=1.000 n=5+5)
Logging/error-value/structured-36 3.13µs ± 0% 3.14µs ± 4% ~ (p=0.730 n=4+5)
Logging/error/structured-36 3.09µs ± 2% 3.13µs ± 3% ~ (p=0.548 n=5+5)
Logging/simple/structured-36 2.86µs ± 4% 2.91µs ± 2% ~ (p=0.310 n=5+5)
Logging/values/structured-36 4.77µs ± 4% 4.77µs ± 5% ~ (p=0.841 n=5+5)
name old alloc/op new alloc/op delta
Logging/container/structured-36 9.35kB ± 0% 9.35kB ± 0% ~ (p=0.095 n=5+4)
Logging/error-value/structured-36 312B ± 0% 312B ± 0% ~ (all equal)
Logging/error/structured-36 312B ± 0% 312B ± 0% ~ (all equal)
Logging/simple/structured-36 288B ± 0% 288B ± 0% ~ (all equal)
Logging/values/structured-36 464B ± 0% 464B ± 0% ~ (all equal)
name old allocs/op new allocs/op delta
Logging/container/structured-36 62.0 ± 0% 62.0 ± 0% ~ (all equal)
Logging/error-value/structured-36 6.00 ± 0% 6.00 ± 0% ~ (all equal)
Logging/error/structured-36 6.00 ± 0% 6.00 ± 0% ~ (all equal)
Logging/simple/structured-36 5.00 ± 0% 5.00 ± 0% ~ (all equal)
Logging/values/structured-36 11.0 ± 0% 11.0 ± 0% ~ (all equal)
It's a bigger win for a real kubelet log:
$ $GOPATH/bin/benchstat /tmp/strconv-kubelet /tmp/reorder-kubelet
name old time/op new time/op delta
Logging/v5/kubelet/structured-36 1.17s ± 2% 1.15s ± 1% -1.37% (p=0.016 n=5+5)
name old alloc/op new alloc/op delta
Logging/v5/kubelet/structured-36 525MB ± 0% 525MB ± 0% ~ (p=0.548 n=5+5)
name old allocs/op new allocs/op delta
Logging/v5/kubelet/structured-36 3.86M ± 0% 3.86M ± 0% ~ (p=0.079 n=5+5)
Args:
total: 310746
strings: 106904 (34%)
with line breaks: 11 (0% of all arguments)
with API objects: 529 (0% of all arguments)
types and their number of usage: Container:529
numbers: 14563 (4%)
ObjectRef: 169306 (54%)
others: 19973 (6%)
---
klog.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/klog.go b/klog.go
index f8ee06e9f..4cf0b5e54 100644
--- a/klog.go
+++ b/klog.go
@@ -835,7 +835,14 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
}
b.WriteByte('=')
+ // The type checks are sorted so that more frequently used ones
+ // come first because that is then faster in the common
+ // cases. In Kubernetes, ObjectRef (a Stringer) is more common
+ // than plain strings
+ // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
switch v := v.(type) {
+ case fmt.Stringer:
+ b.WriteString(strconv.Quote(v.String()))
case string:
b.WriteString(strconv.Quote(v))
case error:
@@ -845,8 +852,6 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
// because it does not escape unicode characters, which is
// expected by one test!?
b.WriteString(fmt.Sprintf("%+q", v))
- case fmt.Stringer:
- b.WriteString(strconv.Quote(v.String()))
default:
b.WriteString(fmt.Sprintf("%+v", v))
}
From fafe98e1ea27f704efbd8ce38bfb9fcb9058a6ab Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Tue, 9 Nov 2021 14:37:53 +0100
Subject: [PATCH 020/125] structured logging: support key/value pairs with line
breaks
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The initial structured logging output (almost) always produced a single line
per log message, with quoting of strings to represent line breaks as \n. This
made the output hard to read (see
https://github.com/kubernetes/kubernetes/issues/104868).
It was still possible to get line breaks when formatting a value with `%+v` and
that ended up emitting line breaks; this was probably not intended.
Now string values are only quoted if they contain no line break. If they do,
start/end markers delimit the text which appears on its own lines, with
indention to ensure that those additional lines are not accidentally treated as
a new log message when they happen to contain the klog header. It also makes
the output more readable.
Tabs are used as indention for the value because it makes it stand out visually
and because it ensures that inline ">" characters are not mistaken for the
end-of-value delimiter.
The result of `fmt.Sprintf("%+v")` is printed verbatim without quoting when it
contains no line break, otherwise as multi-line value with delimiter and
indention.
Traditional output:
I1112 14:06:35.783354 328441 structured_logging.go:42] someData printed using InfoF: {hello world 0}
I1112 14:06:35.783472 328441 structured_logging.go:43] longData printed using InfoF: {long Multiple
lines
with quite a bit
of text. 0}
I1112 14:06:35.783483 328441 structured_logging.go:44] stringData printed using InfoF,
with the message across multiple lines:
long: Multiple
lines
with quite a bit
of text.
I1112 14:06:35.898176 142908 structured_logging.go:54] logData printed using InfoF:
{log output from some program I0000 12:00:00.000000 123456 main.go:42] Starting
E0000 12:00:01.000000 123456 main.go:43] Failed for some reason
0}
Old InfoS output before this commit:
I1112 14:06:35.783512 328441 structured_logging.go:50] "using InfoS" someData={Name:hello Data:world internal:0}
I1112 14:06:35.783529 328441 structured_logging.go:51] "using InfoS" longData={Name:long Data:Multiple
lines
with quite a bit
of text. internal:0}
I1112 14:06:35.783549 328441 structured_logging.go:52] "using InfoS with\nthe message across multiple lines" int=1 stringData="long: Multiple\nlines\nwith quite a bit\nof text." str="another value"
I1112 14:06:35.783565 328441 structured_logging.go:61] "Did something" item="foobar"
I1112 14:06:35.783576 328441 structured_logging.go:63] "This is a full sentence." item="foobar"
I1112 14:06:35.898278 142908 structured_logging.go:65] "using InfoS" logData={Name:log output from some program Data:I0000 12:00:00.000000 123456 main.go:42] Starting
E0000 12:00:01.000000 123456 main.go:43] Failed for some reason
internal:0}
New InfoS output:
I1126 10:31:50.378182 121736 structured_logging.go:58] "using InfoS" someData={Name:hello Data:world internal:0}
I1126 10:31:50.378204 121736 structured_logging.go:59] "using InfoS" longData=<
{Name:long Data:Multiple
lines
with quite a bit
of text. internal:0}
>
I1126 10:31:50.378228 121736 structured_logging.go:60] "using InfoS with\nthe message across multiple lines" int=1 stringData=<
long: Multiple
lines
with quite a bit
of text.
> str="another value"
I1126 10:31:50.378249 121736 structured_logging.go:65] "using InfoS" logData=<
{Name:log output from some program Data:I0000 12:00:00.000000 123456 main.go:42] Starting
E0000 12:00:01.000000 123456 main.go:43] Failed for some reason
internal:0}
>
Performance is the same as before in most cases. Handling of a v1.Container
struct with line breaks in the output gets faster, probably because printing
each line individually is more efficient than quoting.
$ $GOPATH/bin/benchstat /tmp/reorder /tmp/multi-line
name old time/op new time/op delta
Logging/container/structured-36 39.6µs ± 1% 21.5µs ± 0% -45.82% (p=0.008 n=5+5)
Logging/error-value/structured-36 3.14µs ± 4% 3.11µs ± 2% ~ (p=0.548 n=5+5)
Logging/error/structured-36 3.13µs ± 3% 3.13µs ± 3% ~ (p=1.000 n=5+5)
Logging/simple/structured-36 2.91µs ± 2% 2.87µs ± 2% ~ (p=0.310 n=5+5)
Logging/values/structured-36 4.77µs ± 5% 4.75µs ± 4% ~ (p=1.000 n=5+5)
name old alloc/op new alloc/op delta
Logging/container/structured-36 9.35kB ± 0% 9.59kB ± 0% +2.59% (p=0.016 n=4+5)
Logging/error-value/structured-36 312B ± 0% 312B ± 0% ~ (all equal)
Logging/error/structured-36 312B ± 0% 312B ± 0% ~ (all equal)
Logging/simple/structured-36 288B ± 0% 288B ± 0% ~ (all equal)
Logging/values/structured-36 464B ± 0% 464B ± 0% ~ (all equal)
name old allocs/op new allocs/op delta
Logging/container/structured-36 62.0 ± 0% 63.0 ± 0% +1.61% (p=0.008 n=5+5)
Logging/error-value/structured-36 6.00 ± 0% 6.00 ± 0% ~ (all equal)
Logging/error/structured-36 6.00 ± 0% 6.00 ± 0% ~ (all equal)
Logging/simple/structured-36 5.00 ± 0% 5.00 ± 0% ~ (all equal)
Logging/values/structured-36 11.0 ± 0% 11.0 ± 0% ~ (all equal)
---
klog.go | 83 +++++++++++++++++++++++++++++++++++++++++++++-------
klog_test.go | 46 +++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 13 deletions(-)
diff --git a/klog.go b/klog.go
index 4cf0b5e54..4d174e8e3 100644
--- a/klog.go
+++ b/klog.go
@@ -803,10 +803,12 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s
func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
// Only create a new buffer if we don't have one cached.
b := l.getBuffer()
+ // The message is always quoted, even if it contains line breaks.
+ // If developers want multi-line output, they should use a small, fixed
+ // message and put the multi-line output into a value.
b.WriteString(strconv.Quote(msg))
if err != nil {
- b.WriteString(" err=")
- b.WriteString(strconv.Quote(err.Error()))
+ kvListFormat(&b.Buffer, "err", err)
}
kvListFormat(&b.Buffer, keysAndValues...)
l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer)
@@ -826,6 +828,10 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
v = missingValue
}
b.WriteByte(' ')
+ // Keys are assumed to be well-formed according to
+ // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
+ // for the sake of performance. Keys with spaces,
+ // special characters, etc. will break parsing.
if k, ok := k.(string); ok {
// Avoid one allocation when the key is a string, which
// normally it should be.
@@ -833,7 +839,6 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
} else {
b.WriteString(fmt.Sprintf("%s", k))
}
- b.WriteByte('=')
// The type checks are sorted so that more frequently used ones
// come first because that is then faster in the common
@@ -842,19 +847,77 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
// (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
switch v := v.(type) {
case fmt.Stringer:
- b.WriteString(strconv.Quote(v.String()))
+ writeStringValue(b, true, v.String())
case string:
- b.WriteString(strconv.Quote(v))
+ writeStringValue(b, true, v)
case error:
- b.WriteString(strconv.Quote(v.Error()))
+ writeStringValue(b, true, v.Error())
case []byte:
- // We cannot use the simpler strconv.Quote here
- // because it does not escape unicode characters, which is
- // expected by one test!?
+ // In https://github.com/kubernetes/klog/pull/237 it was decided
+ // to format byte slices with "%+q". The advantages of that are:
+ // - readable output if the bytes happen to be printable
+ // - non-printable bytes get represented as unicode escape
+ // sequences (\uxxxx)
+ //
+ // The downsides are that we cannot use the faster
+ // strconv.Quote here and that multi-line output is not
+ // supported. If developers know that a byte array is
+ // printable and they want multi-line output, they can
+ // convert the value to string before logging it.
+ b.WriteByte('=')
b.WriteString(fmt.Sprintf("%+q", v))
default:
- b.WriteString(fmt.Sprintf("%+v", v))
+ writeStringValue(b, false, fmt.Sprintf("%+v", v))
+ }
+ }
+}
+
+func writeStringValue(b *bytes.Buffer, quote bool, v string) {
+ data := []byte(v)
+ index := bytes.IndexByte(data, '\n')
+ if index == -1 {
+ b.WriteByte('=')
+ if quote {
+ // Simple string, quote quotation marks and non-printable characters.
+ b.WriteString(strconv.Quote(v))
+ return
}
+ // Non-string with no line breaks.
+ b.WriteString(v)
+ return
+ }
+
+ // Complex multi-line string, show as-is with indention like this:
+ // I... "hello world" key=<
+ // line 1
+ // line 2
+ // >
+ //
+ // Tabs indent the lines of the value while the end of string delimiter
+ // is indented with a space. That has two purposes:
+ // - visual difference between the two for a human reader because indention
+ // will be different
+ // - no ambiguity when some value line starts with the end delimiter
+ //
+ // One downside is that the output cannot distinguish between strings that
+ // end with a line break and those that don't because the end delimiter
+ // will always be on the next line.
+ b.WriteString("=<\n")
+ for index != -1 {
+ b.WriteByte('\t')
+ b.Write(data[0 : index+1])
+ data = data[index+1:]
+ index = bytes.IndexByte(data, '\n')
+ }
+ if len(data) == 0 {
+ // String ended with line break, don't add another.
+ b.WriteString(" >")
+ } else {
+ // No line break at end of last line, write rest of string and
+ // add one.
+ b.WriteByte('\t')
+ b.Write(data)
+ b.WriteString("\n >")
}
}
diff --git a/klog_test.go b/klog_test.go
index 9a44c6be3..2f65ef894 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -943,6 +943,12 @@ func TestVInfoS(t *testing.T) {
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
}
pid = 1234
+ myData := struct {
+ Data string
+ }{
+ Data: `This is some long text
+with a line break.`,
+ }
var testDataInfo = []struct {
msg string
format string
@@ -963,6 +969,26 @@ func TestVInfoS(t *testing.T) {
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" err=\"test error\"\n",
keysValues: []interface{}{"err", errors.New("test error")},
},
+ {
+ msg: `first message line
+second message line`,
+ format: `I0102 15:04:05.067890 1234 klog_test.go:%d] "first message line\nsecond message line" multiLine=<
+ first value line
+ second value line
+ >
+`,
+ keysValues: []interface{}{"multiLine", `first value line
+second value line`},
+ },
+ {
+ msg: `message`,
+ format: `I0102 15:04:05.067890 1234 klog_test.go:%d] "message" myData=<
+ {Data:This is some long text
+ with a line break.}
+ >
+`,
+ keysValues: []interface{}{"myData", myData},
+ },
}
logging.verbosity.Set("2")
@@ -987,7 +1013,7 @@ func TestVInfoS(t *testing.T) {
want = ""
}
if contents(infoLog) != want {
- t.Errorf("V(%d).InfoS has unexpected output: \n got:\t%s\nwant:\t%s", l, contents(infoLog), want)
+ t.Errorf("V(%d).InfoS has unexpected output:\ngot:\n%s\nwant:\n%s\n", l, contents(infoLog), want)
}
}
}
@@ -1031,7 +1057,7 @@ func TestErrorS(t *testing.T) {
}
want := fmt.Sprintf(e.format, line)
if contents(errorLog) != want {
- t.Errorf("ErrorS has wrong format: \n got:\t%s\nwant:\t%s", contents(errorLog), want)
+ t.Errorf("ErrorS has wrong format:\ngot:\n%s\nwant:\n%s\n", contents(errorLog), want)
}
}
}
@@ -1075,6 +1101,20 @@ func TestKvListFormat(t *testing.T) {
keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("��=� ⌘")},
want: " pod=\"kubedns\" bytes=\"\\ufffd\\ufffd=\\ufffd \\u2318\"",
},
+ {
+ keysValues: []interface{}{"multiLineString", `Hello world!
+ Starts with tab.
+ Starts with spaces.
+No whitespace.`,
+ "pod", "kubedns",
+ },
+ want: ` multiLineString=<
+ Hello world!
+ Starts with tab.
+ Starts with spaces.
+ No whitespace.
+ > pod="kubedns"`,
+ },
{
keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}},
want: " pod=\"kubedns\" maps=map[three:4]",
@@ -1113,7 +1153,7 @@ func TestKvListFormat(t *testing.T) {
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)
+ t.Errorf("kvlist format error:\ngot:\n%s\nwant:\n%s\n", b.String(), d.want)
}
}
}
From 39919d868e0c9919450c9c26becb5ea702a0909e Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Fri, 10 Dec 2021 12:05:44 +0100
Subject: [PATCH 021/125] add format test for KObjs
This wasn't covered before. At first glance the different formatting of the
individual objects (no quotation marks) looks a bit odd compared to how KObj is
formatted, but that's what we have right now.
---
klog_test.go | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/klog_test.go b/klog_test.go
index 9a44c6be3..4246e3440 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1107,6 +1107,18 @@ func TestKvListFormat(t *testing.T) {
keysValues: []interface{}{"pod", KObj((*kMetadataMock)(nil)), "status", "ready"},
want: " pod=\"\" status=\"ready\"",
},
+ {
+ keysValues: []interface{}{"pods", KObjs([]kMetadataMock{
+ {
+ name: "kube-dns",
+ ns: "kube-system",
+ },
+ {
+ name: "mi-conf",
+ },
+ })},
+ want: " pods=[kube-system/kube-dns mi-conf]",
+ },
}
for _, d := range testKVList {
From b605dee249b99c0ee549974d77f3c8c72a642a58 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Sat, 11 Dec 2021 11:31:52 +0100
Subject: [PATCH 022/125] add Verbose.InfoSDepth
It is needed in Kubernetes for code like this (from
k8s.io/apiserver/pkg/server/httplog/httplog.go):
// Log is intended to be called once at the end of your request handler, via defer
func (rl *respLogger) Log() {
...
klog.V(withLoggingLevel).InfoSDepth(1, "HTTP", keysAndValues...)
}
Without InfoSDepth as method for Verbose, such code has to use klog.InfoSDepth,
which records the message with v=0 in JSON output. This is the wrong verbosity
for this debug message.
We don't need Verbose.ErrorSDepth (error messages have no verbosity) and also
no Verbose.Info[f]Depth (when touching code, it should be rewritten to use
Verbose.InfoSDepth instead)
---
klog.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/klog.go b/klog.go
index 45efbb075..ad8598305 100644
--- a/klog.go
+++ b/klog.go
@@ -1387,6 +1387,14 @@ func InfoSDepth(depth int, msg string, keysAndValues ...interface{}) {
logging.infoS(logging.logr, logging.filter, depth, msg, keysAndValues...)
}
+// InfoSDepth is equivalent to the global InfoSDepth function, guarded by the value of v.
+// See the documentation of V for usage.
+func (v Verbose) InfoSDepth(depth int, msg string, keysAndValues ...interface{}) {
+ if v.enabled {
+ logging.infoS(v.logr, v.filter, depth, msg, keysAndValues...)
+ }
+}
+
// Deprecated: Use ErrorS instead.
func (v Verbose) Error(err error, msg string, args ...interface{}) {
if v.enabled {
From 125dec9bbe623ab72ec25154974434e2ffc28782 Mon Sep 17 00:00:00 2001
From: Davanum Srinivas
Date: Fri, 17 Dec 2021 20:06:20 -0500
Subject: [PATCH 023/125] Recover from nil pointers when logging
Signed-off-by: Davanum Srinivas
---
klog.go | 12 +++++++++++-
klog_test.go | 16 ++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/klog.go b/klog.go
index 6b842d8c3..dacdf9112 100644
--- a/klog.go
+++ b/klog.go
@@ -847,7 +847,7 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
// (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
switch v := v.(type) {
case fmt.Stringer:
- writeStringValue(b, true, v.String())
+ writeStringValue(b, true, stringerToString(v))
case string:
writeStringValue(b, true, v)
case error:
@@ -872,6 +872,16 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
}
}
+func stringerToString(s fmt.Stringer) (ret string) {
+ defer func() {
+ if err := recover(); err != nil {
+ ret = "nil"
+ }
+ }()
+ ret = s.String()
+ return
+}
+
func writeStringValue(b *bytes.Buffer, quote bool, v string) {
data := []byte(v)
index := bytes.IndexByte(data, '\n')
diff --git a/klog_test.go b/klog_test.go
index a89968add..a371845d9 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1063,8 +1063,20 @@ func TestErrorS(t *testing.T) {
}
}
+// point conforms to fmt.Stringer interface as it implements the String() method
+type point struct {
+ x int
+ y int
+}
+
+// we now have a value receiver
+func (p point) String() string {
+ return fmt.Sprintf("x=%d, y=%d", p.x, p.y)
+}
+
// Test that kvListFormat works as advertised.
func TestKvListFormat(t *testing.T) {
+ var emptyPoint *point
var testKVList = []struct {
keysValues []interface{}
want string
@@ -1159,6 +1171,10 @@ No whitespace.`,
})},
want: " pods=[kube-system/kube-dns mi-conf]",
},
+ {
+ keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint},
+ want: " point-1=\"x=100, y=200\" point-2=\"nil\"",
+ },
}
for _, d := range testKVList {
From 868bc68c8b1236129dd305d801e01e076a033e39 Mon Sep 17 00:00:00 2001
From: s3rj1k
Date: Tue, 28 Dec 2021 16:43:53 +0200
Subject: [PATCH 024/125] Add missing Depth logging functions.
Signed-off-by: s3rj1k
---
klog.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 96 insertions(+), 2 deletions(-)
diff --git a/klog.go b/klog.go
index dacdf9112..39944b8d7 100644
--- a/klog.go
+++ b/klog.go
@@ -699,7 +699,11 @@ func (buf *buffer) someDigits(i, d int) int {
}
func (l *loggingT) println(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
- buf, file, line := l.header(s, 0)
+ l.printlnDepth(s, logger, filter, 1, args...)
+}
+
+func (l *loggingT) printlnDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
+ buf, file, line := l.header(s, depth)
// if logger is set, we clear the generated header as we rely on the backing
// logger implementation to print headers
if logger != nil {
@@ -736,7 +740,11 @@ func (l *loggingT) printDepth(s severity, logger *logr.Logger, filter LogFilter,
}
func (l *loggingT) printf(s severity, logger *logr.Logger, filter LogFilter, format string, args ...interface{}) {
- buf, file, line := l.header(s, 0)
+ l.printfDepth(s, logger, filter, 1, format, args...)
+}
+
+func (l *loggingT) printfDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, format string, args ...interface{}) {
+ buf, file, line := l.header(s, depth)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
if logger != nil {
@@ -1459,6 +1467,14 @@ func (v Verbose) Info(args ...interface{}) {
}
}
+// InfoDepth is equivalent to the global InfoDepth function, guarded by the value of v.
+// See the documentation of V for usage.
+func (v Verbose) InfoDepth(depth int, args ...interface{}) {
+ if v.enabled {
+ logging.printDepth(infoLog, v.logr, v.filter, depth, args...)
+ }
+}
+
// Infoln is equivalent to the global Infoln function, guarded by the value of v.
// See the documentation of V for usage.
func (v Verbose) Infoln(args ...interface{}) {
@@ -1467,6 +1483,14 @@ func (v Verbose) Infoln(args ...interface{}) {
}
}
+// InfolnDepth is equivalent to the global InfolnDepth function, guarded by the value of v.
+// See the documentation of V for usage.
+func (v Verbose) InfolnDepth(depth int, args ...interface{}) {
+ if v.enabled {
+ logging.printlnDepth(infoLog, v.logr, v.filter, depth, args...)
+ }
+}
+
// Infof is equivalent to the global Infof function, guarded by the value of v.
// See the documentation of V for usage.
func (v Verbose) Infof(format string, args ...interface{}) {
@@ -1475,6 +1499,14 @@ func (v Verbose) Infof(format string, args ...interface{}) {
}
}
+// InfofDepth is equivalent to the global InfofDepth function, guarded by the value of v.
+// See the documentation of V for usage.
+func (v Verbose) InfofDepth(depth int, format string, args ...interface{}) {
+ if v.enabled {
+ logging.printfDepth(infoLog, v.logr, v.filter, depth, format, args...)
+ }
+}
+
// 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{}) {
@@ -1530,12 +1562,24 @@ func Infoln(args ...interface{}) {
logging.println(infoLog, logging.logr, logging.filter, args...)
}
+// InfolnDepth acts as Infoln but uses depth to determine which call frame to log.
+// InfolnDepth(0, "msg") is the same as Infoln("msg").
+func InfolnDepth(depth int, args ...interface{}) {
+ logging.printlnDepth(infoLog, logging.logr, logging.filter, depth, args...)
+}
+
// Infof logs to the INFO log.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Infof(format string, args ...interface{}) {
logging.printf(infoLog, logging.logr, logging.filter, format, args...)
}
+// InfofDepth acts as Infof but uses depth to determine which call frame to log.
+// InfofDepth(0, "msg", args...) is the same as Infof("msg", args...).
+func InfofDepth(depth int, format string, args ...interface{}) {
+ logging.printfDepth(infoLog, logging.logr, logging.filter, depth, 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.
@@ -1566,12 +1610,24 @@ func Warningln(args ...interface{}) {
logging.println(warningLog, logging.logr, logging.filter, args...)
}
+// WarninglnDepth acts as Warningln but uses depth to determine which call frame to log.
+// WarninglnDepth(0, "msg") is the same as Warningln("msg").
+func WarninglnDepth(depth int, args ...interface{}) {
+ logging.printDepth(warningLog, logging.logr, logging.filter, depth, args...)
+}
+
// Warningf logs to the WARNING and INFO logs.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Warningf(format string, args ...interface{}) {
logging.printf(warningLog, logging.logr, logging.filter, format, args...)
}
+// WarningfDepth acts as Warningf but uses depth to determine which call frame to log.
+// WarningfDepth(0, "msg", args...) is the same as Warningf("msg", args...).
+func WarningfDepth(depth int, format string, args ...interface{}) {
+ logging.printfDepth(warningLog, logging.logr, logging.filter, depth, format, args...)
+}
+
// Error logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
func Error(args ...interface{}) {
@@ -1590,12 +1646,24 @@ func Errorln(args ...interface{}) {
logging.println(errorLog, logging.logr, logging.filter, args...)
}
+// ErrorlnDepth acts as Errorln but uses depth to determine which call frame to log.
+// ErrorlnDepth(0, "msg") is the same as Errorln("msg").
+func ErrorlnDepth(depth int, args ...interface{}) {
+ logging.printDepth(errorLog, logging.logr, logging.filter, depth, args...)
+}
+
// Errorf logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Errorf(format string, args ...interface{}) {
logging.printf(errorLog, logging.logr, logging.filter, format, args...)
}
+// ErrorfDepth acts as Errorf but uses depth to determine which call frame to log.
+// ErrorfDepth(0, "msg", args...) is the same as Errorf("msg", args...).
+func ErrorfDepth(depth int, format string, args ...interface{}) {
+ logging.printfDepth(errorLog, logging.logr, logging.filter, depth, 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.
@@ -1635,6 +1703,12 @@ func Fatalln(args ...interface{}) {
logging.println(fatalLog, logging.logr, logging.filter, args...)
}
+// FatallnDepth acts as Fatalln but uses depth to determine which call frame to log.
+// FatallnDepth(0, "msg") is the same as Fatalln("msg").
+func FatallnDepth(depth int, args ...interface{}) {
+ logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+}
+
// Fatalf 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.Printf; a newline is appended if missing.
@@ -1642,6 +1716,12 @@ func Fatalf(format string, args ...interface{}) {
logging.printf(fatalLog, logging.logr, logging.filter, format, args...)
}
+// FatalfDepth acts as Fatalf but uses depth to determine which call frame to log.
+// FatalfDepth(0, "msg", args...) is the same as Fatalf("msg", args...).
+func FatalfDepth(depth int, format string, args ...interface{}) {
+ logging.printfDepth(fatalLog, logging.logr, logging.filter, depth, format, args...)
+}
+
// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks.
// It allows Exit and relatives to use the Fatal logs.
var fatalNoStacks uint32
@@ -1666,6 +1746,13 @@ func Exitln(args ...interface{}) {
logging.println(fatalLog, logging.logr, logging.filter, args...)
}
+// ExitlnDepth acts as Exitln but uses depth to determine which call frame to log.
+// ExitlnDepth(0, "msg") is the same as Exitln("msg").
+func ExitlnDepth(depth int, args ...interface{}) {
+ atomic.StoreUint32(&fatalNoStacks, 1)
+ logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+}
+
// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Exitf(format string, args ...interface{}) {
@@ -1673,6 +1760,13 @@ func Exitf(format string, args ...interface{}) {
logging.printf(fatalLog, logging.logr, logging.filter, format, args...)
}
+// ExitfDepth acts as Exitf but uses depth to determine which call frame to log.
+// ExitfDepth(0, "msg", args...) is the same as Exitf("msg", args...).
+func ExitfDepth(depth int, format string, args ...interface{}) {
+ atomic.StoreUint32(&fatalNoStacks, 1)
+ logging.printfDepth(fatalLog, logging.logr, logging.filter, depth, format, args...)
+}
+
// LogFilter is a collection of functions that can filter all logging calls,
// e.g. for sanitization of arguments and prevent accidental leaking of secrets.
type LogFilter interface {
From 929e7a94bda62f0f8166548930a95ced9981300e Mon Sep 17 00:00:00 2001
From: Julian Lawrence
Date: Tue, 18 Jan 2022 23:27:29 -0800
Subject: [PATCH 025/125] fixed empty info log panic
---
klog.go | 2 +-
klog_test.go | 36 ++++++++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/klog.go b/klog.go
index dacdf9112..73a0c930f 100644
--- a/klog.go
+++ b/klog.go
@@ -729,7 +729,7 @@ func (l *loggingT) printDepth(s severity, logger *logr.Logger, filter LogFilter,
args = filter.Filter(args)
}
fmt.Fprint(buf, args...)
- if buf.Bytes()[buf.Len()-1] != '\n' {
+ if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
l.output(s, logger, buf, depth, file, line, false)
diff --git a/klog_test.go b/klog_test.go
index a371845d9..17caaf4e0 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -1416,6 +1416,42 @@ func TestLogFilter(t *testing.T) {
}
}
+func TestInfoWithLogr(t *testing.T) {
+ logger := new(testLogr)
+
+ testDataInfo := []struct {
+ msg string
+ expected testLogrEntry
+ }{{
+ msg: "foo",
+ expected: testLogrEntry{
+ severity: infoLog,
+ msg: "foo\n",
+ },
+ }, {
+ msg: "",
+ expected: testLogrEntry{
+ severity: infoLog,
+ msg: "\n",
+ },
+ }}
+
+ for _, data := range testDataInfo {
+ t.Run(data.msg, func(t *testing.T) {
+ l := logr.New(logger)
+ SetLogger(l)
+ defer ClearLogger()
+ defer logger.reset()
+
+ Info(data.msg)
+
+ if !reflect.DeepEqual(logger.entries, []testLogrEntry{data.expected}) {
+ t.Errorf("expected: %+v; but got: %+v", []testLogrEntry{data.expected}, logger.entries)
+ }
+ })
+ }
+}
+
func TestInfoSWithLogr(t *testing.T) {
logger := new(testLogr)
From d78dad3276f1f4139a6614b08b02e4678d2c3393 Mon Sep 17 00:00:00 2001
From: Noaa Barki <47917189+noaabarki@users.noreply.github.com>
Date: Sat, 15 Jan 2022 18:46:34 +0200
Subject: [PATCH 026/125] Update README.md
Added documentation link to make things much more clear and easy.
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 64d29622e..b52bfcdcc 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ How to use klog
- You can now use `log_file` instead of `log_dir` for logging to a single file (See `examples/log_file/usage_log_file.go`)
- If you want to redirect everything logged using klog somewhere else (say syslog!), you can use `klog.SetOutput()` method and supply a `io.Writer`. (See `examples/set_output/usage_set_output.go`)
- For more logging conventions (See [Logging Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md))
+- See our documentation on [pkg.go.dev/k8s.io](https://pkg.go.dev/k8s.io/klog).
**NOTE**: please use the newer go versions that support semantic import versioning in modules, ideally go 1.11.4 or greater.
@@ -85,7 +86,7 @@ The comment from glog.go introduces the ideas:
glog.Fatalf("Initialization failed: %s", err)
- See the documentation for the V function for an explanation
+ See the documentation of the V function for an explanation
of these examples:
if glog.V(2) {
From 714cb7a39dd4dc298939c3f543130af5ee40e90b Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Tue, 8 Feb 2022 15:30:00 +0100
Subject: [PATCH 027/125] SetLogger/ClearLogger/SetFilter: document as not
thread-safe
It has never been claimed that these functions are thread-safe and in practice
they weren't because read access was not protected by the same mutex as the
write access.
These functions were meant to be called during program initialization,
therefore the documentation is lacking, not the implementation.
Making them thread-safe would imply adding mutex locking to the implementation
of V, which most likely would have a noticeable effect.
---
klog.go | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/klog.go b/klog.go
index 9fb51ba49..4c5503fdf 100644
--- a/klog.go
+++ b/klog.go
@@ -965,15 +965,18 @@ func (rb *redirectBuffer) Write(bytes []byte) (n int, err error) {
//
// To remove a backing logr implemention, use ClearLogger. Setting an
// empty logger with SetLogger(logr.Logger{}) does not work.
+//
+// Modifying the logger is not thread-safe and should be done while no other
+// goroutines invoke log calls, usually during program initialization.
func SetLogger(logr logr.Logger) {
- logging.mu.Lock()
- defer logging.mu.Unlock()
-
logging.logr = &logr
}
// ClearLogger removes a backing logr implementation if one was set earlier
// with SetLogger.
+//
+// Modifying the logger is not thread-safe and should be done while no other
+// goroutines invoke log calls, usually during program initialization.
func ClearLogger() {
logging.mu.Lock()
defer logging.mu.Unlock()
@@ -1775,10 +1778,11 @@ type LogFilter interface {
FilterS(msg string, keysAndValues []interface{}) (string, []interface{})
}
+// SetLogFilter installs a filter that is used for all log calls.
+//
+// Modifying the filter is not thread-safe and should be done while no other
+// goroutines invoke log calls, usually during program initialization.
func SetLogFilter(filter LogFilter) {
- logging.mu.Lock()
- defer logging.mu.Unlock()
-
logging.filter = filter
}
From 892e4f49f549cd897ea541bda467ae8fdddd7076 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Tue, 8 Feb 2022 15:34:14 +0100
Subject: [PATCH 028/125] log filter: ignored by V, used during log call
It's not entirely clear why the filter was copied into Verbose. The only
semantic difference is when Verbose get stored and then the filter gets
modified:
klogV := klog.V(5)
klog.SetLogFilter(...)
klogV.Info(...)
This seems like a very unlikely code pattern and goes against the intention of
first initializing logging, then using it.
Therefore we can undo the size increase in Verbose and only retrieve the filter
when it is needed.
---
klog.go | 25 ++++++++++++-------------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/klog.go b/klog.go
index 9fb51ba49..1f201d06d 100644
--- a/klog.go
+++ b/klog.go
@@ -1392,15 +1392,14 @@ func (l *loggingT) setV(pc uintptr) Level {
type Verbose struct {
enabled bool
logr *logr.Logger
- filter LogFilter
}
func newVerbose(level Level, b bool) Verbose {
if logging.logr == nil {
- return Verbose{b, nil, logging.filter}
+ return Verbose{b, nil}
}
v := logging.logr.V(int(level))
- return Verbose{b, &v, logging.filter}
+ return Verbose{b, &v}
}
// V reports whether verbosity at the call site is at least the requested level.
@@ -1463,7 +1462,7 @@ func (v Verbose) Enabled() bool {
// See the documentation of V for usage.
func (v Verbose) Info(args ...interface{}) {
if v.enabled {
- logging.print(infoLog, v.logr, v.filter, args...)
+ logging.print(infoLog, v.logr, logging.filter, args...)
}
}
@@ -1471,7 +1470,7 @@ func (v Verbose) Info(args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfoDepth(depth int, args ...interface{}) {
if v.enabled {
- logging.printDepth(infoLog, v.logr, v.filter, depth, args...)
+ logging.printDepth(infoLog, v.logr, logging.filter, depth, args...)
}
}
@@ -1479,7 +1478,7 @@ func (v Verbose) InfoDepth(depth int, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) Infoln(args ...interface{}) {
if v.enabled {
- logging.println(infoLog, v.logr, v.filter, args...)
+ logging.println(infoLog, v.logr, logging.filter, args...)
}
}
@@ -1487,7 +1486,7 @@ func (v Verbose) Infoln(args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfolnDepth(depth int, args ...interface{}) {
if v.enabled {
- logging.printlnDepth(infoLog, v.logr, v.filter, depth, args...)
+ logging.printlnDepth(infoLog, v.logr, logging.filter, depth, args...)
}
}
@@ -1495,7 +1494,7 @@ func (v Verbose) InfolnDepth(depth int, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) Infof(format string, args ...interface{}) {
if v.enabled {
- logging.printf(infoLog, v.logr, v.filter, format, args...)
+ logging.printf(infoLog, v.logr, logging.filter, format, args...)
}
}
@@ -1503,7 +1502,7 @@ func (v Verbose) Infof(format string, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfofDepth(depth int, format string, args ...interface{}) {
if v.enabled {
- logging.printfDepth(infoLog, v.logr, v.filter, depth, format, args...)
+ logging.printfDepth(infoLog, v.logr, logging.filter, depth, format, args...)
}
}
@@ -1511,7 +1510,7 @@ func (v Verbose) InfofDepth(depth int, format string, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfoS(msg string, keysAndValues ...interface{}) {
if v.enabled {
- logging.infoS(v.logr, v.filter, 0, msg, keysAndValues...)
+ logging.infoS(v.logr, logging.filter, 0, msg, keysAndValues...)
}
}
@@ -1525,14 +1524,14 @@ func InfoSDepth(depth int, msg string, keysAndValues ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfoSDepth(depth int, msg string, keysAndValues ...interface{}) {
if v.enabled {
- logging.infoS(v.logr, v.filter, depth, msg, keysAndValues...)
+ logging.infoS(v.logr, logging.filter, depth, msg, keysAndValues...)
}
}
// Deprecated: Use ErrorS instead.
func (v Verbose) Error(err error, msg string, args ...interface{}) {
if v.enabled {
- logging.errorS(err, v.logr, v.filter, 0, msg, args...)
+ logging.errorS(err, v.logr, logging.filter, 0, msg, args...)
}
}
@@ -1540,7 +1539,7 @@ func (v Verbose) Error(err error, msg string, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) ErrorS(err error, msg string, keysAndValues ...interface{}) {
if v.enabled {
- logging.errorS(err, v.logr, v.filter, 0, msg, keysAndValues...)
+ logging.errorS(err, v.logr, logging.filter, 0, msg, keysAndValues...)
}
}
From 10888e7a3ed17d1b307f3f324460d09ec1c13b0c Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Mon, 14 Feb 2022 10:38:17 +0100
Subject: [PATCH 029/125] README.md: clarify API stability
We need the ability to make experimental changes without new definitions
immediately becoming part of the stable v2 API.
---
README.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/README.md b/README.md
index b52bfcdcc..a9c945e1d 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,18 @@ Historical context is available here:
* https://groups.google.com/forum/#!msg/kubernetes-sig-architecture/wCWiWf3Juzs/hXRVBH90CgAJ
* https://groups.google.com/forum/#!msg/kubernetes-dev/7vnijOMhLS0/1oRiNtigBgAJ
+## Release versioning
+
+Semantic versioning is used in this repository. It contains several Go modules
+with different levels of stability:
+- `k8s.io/klog/v2` - stable API, `vX.Y.Z` tags
+- `k8s.io/tools` - no stable API yet (may change eventually), `tools/v0.Y.Z` tags
+- `examples` - no stable API, no tags, no intention to ever stabilize
+
+Exempt from the API stability guarantee are items (packages, functions, etc.)
+which are marked explicitly as `EXPERIMENTAL` in their docs comment. Those
+may still change in incompatible ways or get removed entirely.
+
----
How to use klog
From 7cad178d464bfd8f2040c0425ccbecdfe2f77bf7 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 16:57:32 +0100
Subject: [PATCH 030/125] test: unified testing of structured logging
The new test cases are applied to structured logging calls in klog and the
stand-alone klogr logger with output through klog. The output is expected to be
the same.
---
test/output.go | 464 ++++++++++++++++++++++++++++++++++++++++++
test/output_helper.go | 32 +++
test/output_test.go | 62 ++++++
3 files changed, 558 insertions(+)
create mode 100644 test/output.go
create mode 100644 test/output_helper.go
create mode 100644 test/output_test.go
diff --git a/test/output.go b/test/output.go
new file mode 100644
index 000000000..f0a883ebf
--- /dev/null
+++ b/test/output.go
@@ -0,0 +1,464 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package test contains a reusable unit test for logging output and behavior.
+//
+// Experimental
+//
+// Notice: This package is EXPERIMENTAL and may be changed or removed in a
+// later release.
+package test
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "regexp"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+
+ "k8s.io/klog/v2"
+)
+
+// InitKlog must be called once in an init function of a test package to
+// configure klog for testing with Output.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func InitKlog() {
+ // klog gets configured so that it writes to a single output file that
+ // will be set during tests with SetOutput.
+ klog.InitFlags(nil)
+ flag.Set("v", "10")
+ flag.Set("log_file", "/dev/null")
+ flag.Set("logtostderr", "false")
+ flag.Set("alsologtostderr", "false")
+ flag.Set("stderrthreshold", "10")
+}
+
+// OutputConfig contains optional settings for Output.
+//
+// Experimental
+//
+// Notice: This type is EXPERIMENTAL and may be changed or removed in a
+// later release.
+type OutputConfig struct {
+ // NewLogger is called to create a new logger. If nil, output via klog
+ // is tested. Support for -vmodule is optional.
+ NewLogger func(out io.Writer, v int, vmodule string) logr.Logger
+
+ // AsBackend enables testing through klog and the logger set there with
+ // SetLogger.
+ AsBackend bool
+
+ // ExpectedOutputMapping replaces the builtin expected output for test
+ // cases with something else. If nil or a certain case is not present,
+ // the original text is used.
+ //
+ // The expected output uses as a placeholder for the line of the
+ // log call. The source code is always the output.go file itself. When
+ // testing a logger directly, is used for the first
+ // WithValues call, for a second and
+ // for a third.
+ ExpectedOutputMapping map[string]string
+
+ // SupportsVModule indicates that the logger supports the vmodule
+ // parameter. Ignored when logging through klog.
+ SupportsVModule bool
+}
+
+// Output covers various special cases of emitting log output.
+// It can be used for arbitrary logr.Logger implementations.
+//
+// The expected output is what klog would print. When testing loggers
+// that emit different output, a mapping from klog output to the
+// corresponding logger output must be provided, otherwise the
+// test will compare against the expected klog output.
+//
+// Loggers will be tested with direct calls to Info or
+// as backend for klog.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release. The test cases and thus the expected output also may
+// change.
+func Output(t *testing.T, config OutputConfig) {
+ tests := map[string]struct {
+ withHelper bool // use wrappers that get skipped during stack unwinding
+ withNames []string
+ // For a first WithValues call: logger1 := logger.WithValues()
+ withValues []interface{}
+ // For another WithValues call: logger2 := logger1.WithValues()
+ moreValues []interface{}
+ // For another WithValues call on the same logger as before: logger3 := logger1.WithValues()
+ evenMoreValues []interface{}
+ v int
+ vmodule string
+ text string
+ values []interface{}
+ err error
+ expectedOutput string
+ }{
+ "log with values": {
+ text: "test",
+ values: []interface{}{"akey", "avalue"},
+ expectedOutput: `I output.go:] "test" akey="avalue"
+`,
+ },
+ "call depth": {
+ text: "helper",
+ withHelper: true,
+ values: []interface{}{"akey", "avalue"},
+ expectedOutput: `I output.go:] "helper" akey="avalue"
+`,
+ },
+ "verbosity enabled": {
+ text: "you see me",
+ v: 9,
+ expectedOutput: `I output.go:] "you see me"
+`,
+ },
+ "verbosity disabled": {
+ text: "you don't see me",
+ v: 11,
+ },
+ "vmodule": {
+ text: "v=11: you see me because of -vmodule output=11",
+ v: 11,
+ vmodule: "output=11",
+ },
+ "other vmodule": {
+ text: "v=11: you still don't see me because of -vmodule output_helper=11",
+ v: 11,
+ vmodule: "output_helper=11",
+ },
+ "log with name and values": {
+ withNames: []string{"me"},
+ text: "test",
+ values: []interface{}{"akey", "avalue"},
+ expectedOutput: `I output.go:] "me: test" akey="avalue"
+`,
+ },
+ "log with multiple names and values": {
+ withNames: []string{"hello", "world"},
+ text: "test",
+ values: []interface{}{"akey", "avalue"},
+ expectedOutput: `I output.go:] "hello/world: test" akey="avalue"
+`,
+ },
+ "override single value": {
+ withValues: []interface{}{"akey", "avalue"},
+ text: "test",
+ values: []interface{}{"akey", "avalue2"},
+ expectedOutput: `I output.go:] "test" akey="avalue2"
+`,
+ },
+ "override WithValues": {
+ withValues: []interface{}{"duration", time.Hour, "X", "y"},
+ text: "test",
+ values: []interface{}{"duration", time.Minute, "A", "b"},
+ expectedOutput: `I output.go:] "test" X="y" duration="1m0s" A="b"
+`,
+ },
+ "odd WithValues": {
+ withValues: []interface{}{"keyWithoutValue"},
+ moreValues: []interface{}{"anotherKeyWithoutValue"},
+ text: "test",
+ expectedOutput: `I output.go:] "test" keyWithoutValue="(MISSING)"
+I output.go:] "test" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
+I output.go:] "test" keyWithoutValue="(MISSING)"
+`,
+ },
+ "multiple WithValues": {
+ withValues: []interface{}{"firstKey", 1},
+ moreValues: []interface{}{"secondKey", 2},
+ evenMoreValues: []interface{}{"secondKey", 3},
+ text: "test",
+ expectedOutput: `I output.go:] "test" firstKey=1
+I output.go:] "test" firstKey=1 secondKey=2
+I output.go:] "test" firstKey=1
+I output.go:] "test" firstKey=1 secondKey=3
+`,
+ },
+ "empty WithValues": {
+ withValues: []interface{}{},
+ text: "test",
+ expectedOutput: `I output.go:] "test"
+`,
+ },
+ // TODO: unify behavior of loggers.
+ // klog doesn't deduplicate, klogr and textlogger do. We can ensure via static code analysis
+ // that this doesn't occur, so we shouldn't pay the runtime overhead for deduplication here
+ // and remove that from klogr and textlogger (https://github.com/kubernetes/klog/issues/286).
+ // "print duplicate keys in arguments": {
+ // text: "test",
+ // values: []interface{}{"akey", "avalue", "akey", "avalue2"},
+ // expectedOutput: `I output.go:] "test" akey="avalue" akey="avalue2"
+ // `,
+ // },
+ "preserve order of key/value pairs": {
+ withValues: []interface{}{"akey9", "avalue9", "akey8", "avalue8", "akey1", "avalue1"},
+ text: "test",
+ values: []interface{}{"akey5", "avalue5", "akey4", "avalue4"},
+ expectedOutput: `I output.go:] "test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4"
+`,
+ },
+ "handle odd-numbers of KVs": {
+ text: "test",
+ values: []interface{}{"akey", "avalue", "akey2"},
+ expectedOutput: `I output.go:] "test" akey="avalue" akey2="(MISSING)"
+`,
+ },
+ "html characters": {
+ text: "test",
+ values: []interface{}{"akey", "<&>"},
+ expectedOutput: `I output.go:] "test" akey="<&>"
+`,
+ },
+ "quotation": {
+ text: `"quoted"`,
+ values: []interface{}{"key", `"quoted value"`},
+ expectedOutput: `I output.go:] "\"quoted\"" key="\"quoted value\""
+`,
+ },
+ "handle odd-numbers of KVs in both log values and Info args": {
+ withValues: []interface{}{"basekey1", "basevar1", "basekey2"},
+ text: "test",
+ values: []interface{}{"akey", "avalue", "akey2"},
+ expectedOutput: `I output.go:] "test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+`,
+ },
+ "KObj": {
+ text: "test",
+ values: []interface{}{"pod", klog.KObj(&kmeta{Name: "pod-1", Namespace: "kube-system"})},
+ expectedOutput: `I output.go:] "test" pod="kube-system/pod-1"
+`,
+ },
+ "KObjs": {
+ text: "test",
+ values: []interface{}{"pods",
+ klog.KObjs([]interface{}{
+ &kmeta{Name: "pod-1", Namespace: "kube-system"},
+ &kmeta{Name: "pod-2", Namespace: "kube-system"},
+ })},
+ expectedOutput: `I output.go:] "test" pods=[kube-system/pod-1 kube-system/pod-2]
+`,
+ },
+ "regular error types as value": {
+ text: "test",
+ values: []interface{}{"err", errors.New("whoops")},
+ expectedOutput: `I output.go:] "test" err="whoops"
+`,
+ },
+ "ignore MarshalJSON": {
+ text: "test",
+ values: []interface{}{"err", &customErrorJSON{"whoops"}},
+ expectedOutput: `I output.go:] "test" err="whoops"
+`,
+ },
+ "regular error types when using logr.Error": {
+ text: "test",
+ err: errors.New("whoops"),
+ expectedOutput: `E output.go:] "test" err="whoops"
+`,
+ },
+ }
+ for n, test := range tests {
+ t.Run(n, func(t *testing.T) {
+ printWithLogger := func(logger logr.Logger) {
+ for _, name := range test.withNames {
+ logger = logger.WithName(name)
+ }
+ // When we have multiple WithValues calls, we test
+ // first with the initial set of additional values, then
+ // the combination, then again the original logger.
+ // It must not have been modified. This produces
+ // three log entries.
+ logger = logger.WithValues(test.withValues...)
+ loggers := []logr.Logger{logger}
+ if test.moreValues != nil {
+ loggers = append(loggers, logger.WithValues(test.moreValues...), logger)
+ }
+ if test.evenMoreValues != nil {
+ loggers = append(loggers, logger.WithValues(test.evenMoreValues...))
+ }
+ for _, logger := range loggers {
+ if test.withHelper {
+ loggerHelper(logger, test.text, test.values)
+ } else if test.err != nil {
+ logger.Error(test.err, test.text, test.values...)
+ } else {
+ logger.V(test.v).Info(test.text, test.values...)
+ }
+ }
+ }
+ _, _, printWithLoggerLine, _ := runtime.Caller(0)
+
+ printWithKlog := func() {
+ kv := []interface{}{}
+ haveKeyInValues := func(key interface{}) bool {
+ for i := 0; i < len(test.values); i += 2 {
+ if key == test.values[i] {
+ return true
+ }
+ }
+ return false
+ }
+ appendKV := func(withValues []interface{}) {
+ if len(withValues)%2 != 0 {
+ withValues = append(withValues, "(MISSING)")
+ }
+ for i := 0; i < len(withValues); i += 2 {
+ if !haveKeyInValues(withValues[i]) {
+ kv = append(kv, withValues[i], withValues[i+1])
+ }
+ }
+ }
+ // Here we need to emulate the handling of WithValues above.
+ appendKV(test.withValues)
+ kvs := [][]interface{}{copySlice(kv)}
+ if test.moreValues != nil {
+ appendKV(test.moreValues)
+ kvs = append(kvs, copySlice(kv), copySlice(kvs[0]))
+ }
+ if test.evenMoreValues != nil {
+ kv = copySlice(kvs[0])
+ appendKV(test.evenMoreValues)
+ kvs = append(kvs, copySlice(kv))
+ }
+ for _, kv := range kvs {
+ if len(test.values) > 0 {
+ kv = append(kv, test.values...)
+ }
+ text := test.text
+ if len(test.withNames) > 0 {
+ text = strings.Join(test.withNames, "/") + ": " + text
+ }
+ if test.withHelper {
+ klogHelper(text, kv)
+ } else if test.err != nil {
+ klog.ErrorS(test.err, text, kv...)
+ } else {
+ klog.V(klog.Level(test.v)).InfoS(text, kv...)
+ }
+ }
+ }
+ _, _, printWithKlogLine, _ := runtime.Caller(0)
+
+ testOutput := func(t *testing.T, expectedLine int, print func(buffer *bytes.Buffer)) {
+ var tmpWriteBuffer bytes.Buffer
+ klog.SetOutput(&tmpWriteBuffer)
+ print(&tmpWriteBuffer)
+ klog.Flush()
+
+ actual := tmpWriteBuffer.String()
+ // Strip varying header.
+ re := `(?m)^(.).... ..:..:......... ....... output.go`
+ actual = regexp.MustCompile(re).ReplaceAllString(actual, `${1} output.go`)
+
+ // Inject expected line. This matches the if checks above, which are
+ // the same for both printWithKlog and printWithLogger.
+ callLine := expectedLine
+ if test.withHelper {
+ callLine -= 8
+ } else if test.err != nil {
+ callLine -= 6
+ } else {
+ callLine -= 4
+ }
+ expected := test.expectedOutput
+ if repl, ok := config.ExpectedOutputMapping[expected]; ok {
+ expected = repl
+ }
+ expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", callLine))
+ expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-18))
+ expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-15))
+ expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-12))
+ if actual != expected {
+ t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expected, actual)
+ }
+ }
+
+ if config.NewLogger == nil {
+ // Test klog.
+ testOutput(t, printWithKlogLine, func(buffer *bytes.Buffer) {
+ printWithKlog()
+ })
+ return
+ }
+
+ if config.AsBackend {
+ testOutput(t, printWithKlogLine, func(buffer *bytes.Buffer) {
+ klog.SetLogger(config.NewLogger(buffer, 10, ""))
+ defer klog.ClearLogger()
+ printWithKlog()
+ })
+ return
+ }
+
+ if test.vmodule != "" && !config.SupportsVModule {
+ t.Skip("vmodule not supported")
+ }
+
+ testOutput(t, printWithLoggerLine, func(buffer *bytes.Buffer) {
+ printWithLogger(config.NewLogger(buffer, 10, test.vmodule))
+ })
+ })
+ }
+}
+
+func copySlice(in []interface{}) []interface{} {
+ return append([]interface{}{}, in...)
+}
+
+type kmeta struct {
+ Name, Namespace string
+}
+
+func (k kmeta) GetName() string {
+ return k.Name
+}
+
+func (k kmeta) GetNamespace() string {
+ return k.Namespace
+}
+
+var _ klog.KMetadata = kmeta{}
+
+type customErrorJSON struct {
+ s string
+}
+
+var _ error = &customErrorJSON{}
+var _ json.Marshaler = &customErrorJSON{}
+
+func (e *customErrorJSON) Error() string {
+ return e.s
+}
+
+func (e *customErrorJSON) MarshalJSON() ([]byte, error) {
+ return json.Marshal(strings.ToUpper(e.s))
+}
diff --git a/test/output_helper.go b/test/output_helper.go
new file mode 100644
index 000000000..499395e38
--- /dev/null
+++ b/test/output_helper.go
@@ -0,0 +1,32 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+import (
+ "github.com/go-logr/logr"
+
+ "k8s.io/klog/v2"
+)
+
+func loggerHelper(logger logr.Logger, msg string, kv []interface{}) {
+ logger = logger.WithCallDepth(1)
+ logger.Info(msg, kv...)
+}
+
+func klogHelper(msg string, kv []interface{}) {
+ klog.InfoSDepth(1, msg, kv...)
+}
diff --git a/test/output_test.go b/test/output_test.go
new file mode 100644
index 000000000..9b3161761
--- /dev/null
+++ b/test/output_test.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+import (
+ "io"
+ "testing"
+
+ "github.com/go-logr/logr"
+
+ "k8s.io/klog/v2/klogr"
+)
+
+func init() {
+ InitKlog()
+}
+
+// TestKlogOutput tests klog output without a logger.
+func TestKlogOutput(t *testing.T) {
+ Output(t, OutputConfig{})
+}
+
+// TestKlogrOutput tests klogr output via klog.
+func TestKlogrOutput(t *testing.T) {
+ // klogr currently doesn't produce exactly the same output as klog.
+ // TODO: fix that.
+ mapping := map[string]string{
+ `I output.go:] "test" keyWithoutValue="(MISSING)"
+I output.go:] "test" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
+I output.go:] "test" keyWithoutValue="(MISSING)"
+`: `I output.go:] "test" keyWithoutValue=
+I output.go:] "test" keyWithoutValue="anotherKeyWithoutValue"
+I output.go:] "test" keyWithoutValue=
+`,
+ `I output.go:] "test" akey="avalue" akey2="(MISSING)"
+`: `I output.go:] "test" akey="avalue" akey2=
+`,
+ `I output.go:] "test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+`: `I output.go:] "test" basekey1="basevar1" basekey2= akey="avalue" akey2=
+`,
+ }
+ Output(t, OutputConfig{
+ NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger {
+ return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
+ },
+ ExpectedOutputMapping: mapping,
+ })
+}
From 5a670a2897664c9d5e93645a462b5beff76e7565 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Thu, 10 Feb 2022 18:24:23 +0100
Subject: [PATCH 031/125] test: add systematic testing of all klog functions
klog has a lot of functions, but not all of them are covered with unit
tests. Only a few check stack unwinding.
This new test is designed to:
- check stack unwinding for all functions
- check the difference between fmt.Print and fmt.Println (only the
latter inserts spaces between strings)
- check the output with and without a Logger
This therefore covers bugs in https://github.com/kubernetes/klog/pull/280:
- depth was not passed on to Logger
- ErrorlnDepth and WarninglnDepth used fmt.Print instead of fmt.Println
---
test/output.go | 224 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 224 insertions(+)
diff --git a/test/output.go b/test/output.go
index f0a883ebf..0448bea24 100644
--- a/test/output.go
+++ b/test/output.go
@@ -31,6 +31,7 @@ import (
"io"
"regexp"
"runtime"
+ "strconv"
"strings"
"testing"
"time"
@@ -428,6 +429,229 @@ I output.go:] "test" firstKey=1 secondKey=3
})
})
}
+
+ if config.NewLogger == nil || config.AsBackend {
+ // Test all klog output functions.
+ //
+ // Each test case must be defined with the same number of
+ // lines, then the source code location of the call itself
+ // can be computed below.
+ tests := []struct {
+ name string
+ logFunc func()
+ output string
+ }{
+ {
+ name: "Info",
+ logFunc: func() { klog.Info("hello", "world") },
+ output: "I output.go:] helloworld\n", // This looks odd, but simply is how klog works.
+ },
+ {
+ name: "InfoDepth",
+ logFunc: func() { klog.InfoDepth(0, "hello", "world") },
+ output: "I output.go:] helloworld\n",
+ },
+ {
+ name: "Infoln",
+ logFunc: func() { klog.Infoln("hello", "world") },
+ output: "I output.go:] hello world\n",
+ },
+ {
+ name: "InfolnDepth",
+ logFunc: func() { klog.InfolnDepth(0, "hello", "world") },
+ output: "I output.go:] hello world\n",
+ },
+ {
+ name: "Infof",
+ logFunc: func() { klog.Infof("hello %s", "world") },
+ output: "I output.go:] hello world\n",
+ },
+ {
+ name: "InfofDepth",
+ logFunc: func() { klog.InfofDepth(0, "hello %s", "world") },
+ output: "I output.go:] hello world\n",
+ },
+ {
+ name: "InfoS",
+ logFunc: func() { klog.InfoS("hello", "what", "world") },
+ output: "I output.go:] \"hello\" what=\"world\"\n",
+ },
+ {
+ name: "InfoSDepth",
+ logFunc: func() { klog.InfoSDepth(0, "hello", "what", "world") },
+ output: "I output.go:] \"hello\" what=\"world\"\n",
+ },
+ {
+ name: "Warning",
+ logFunc: func() { klog.Warning("hello", "world") },
+ output: "W output.go:] helloworld\n",
+ },
+ {
+ name: "WarningDepth",
+ logFunc: func() { klog.WarningDepth(0, "hello", "world") },
+ output: "W output.go:] helloworld\n",
+ },
+ {
+ name: "Warningln",
+ logFunc: func() { klog.Warningln("hello", "world") },
+ output: "W output.go:] hello world\n",
+ },
+ {
+ name: "WarninglnDepth",
+ logFunc: func() { klog.WarninglnDepth(0, "hello", "world") },
+ output: "W output.go:] helloworld\n", // BUG
+ },
+ {
+ name: "Warningf",
+ logFunc: func() { klog.Warningf("hello %s", "world") },
+ output: "W output.go:] hello world\n",
+ },
+ {
+ name: "WarningfDepth",
+ logFunc: func() { klog.WarningfDepth(0, "hello %s", "world") },
+ output: "W output.go:] hello world\n",
+ },
+ {
+ name: "Error",
+ logFunc: func() { klog.Error("hello", "world") },
+ output: "E output.go:] helloworld\n",
+ },
+ {
+ name: "ErrorDepth",
+ logFunc: func() { klog.ErrorDepth(0, "hello", "world") },
+ output: "E output.go:] helloworld\n",
+ },
+ {
+ name: "Errorln",
+ logFunc: func() { klog.Errorln("hello", "world") },
+ output: "E output.go:] hello world\n",
+ },
+ {
+ name: "ErrorlnDepth",
+ logFunc: func() { klog.ErrorlnDepth(0, "hello", "world") },
+ output: "E output.go:] helloworld\n", // BUG
+ },
+ {
+ name: "Errorf",
+ logFunc: func() { klog.Errorf("hello %s", "world") },
+ output: "E output.go:] hello world\n",
+ },
+ {
+ name: "ErrorfDepth",
+ logFunc: func() { klog.ErrorfDepth(0, "hello %s", "world") },
+ output: "E output.go:] hello world\n",
+ },
+ {
+ name: "ErrorS",
+ logFunc: func() { klog.ErrorS(errors.New("hello"), "world") },
+ output: "E output.go:] \"world\" err=\"hello\"\n",
+ },
+ {
+ name: "ErrorSDepth",
+ logFunc: func() { klog.ErrorSDepth(0, errors.New("hello"), "world") },
+ output: "E output.go:] \"world\" err=\"hello\"\n",
+ },
+ {
+ name: "V().Info",
+ logFunc: func() { klog.V(1).Info("hello", "one", "world") },
+ output: "I output.go:] hellooneworld\n",
+ },
+ {
+ name: "V().InfoDepth",
+ logFunc: func() { klog.V(1).InfoDepth(0, "hello", "one", "world") },
+ output: "I output.go:] hellooneworld\n",
+ },
+ {
+ name: "V().Infoln",
+ logFunc: func() { klog.V(1).Infoln("hello", "one", "world") },
+ output: "I output.go:] hello one world\n",
+ },
+ {
+ name: "V().InfolnDepth",
+ logFunc: func() { klog.V(1).InfolnDepth(0, "hello", "one", "world") },
+ output: "I output.go:] hello one world\n",
+ },
+ {
+ name: "V().Infof",
+ logFunc: func() { klog.V(1).Infof("hello %s %s", "one", "world") },
+ output: "I output.go:] hello one world\n",
+ },
+ {
+ name: "V().InfofDepth",
+ logFunc: func() { klog.V(1).InfofDepth(0, "hello %s %s", "one", "world") },
+ output: "I output.go:] hello one world\n",
+ },
+ {
+ name: "V().InfoS",
+ logFunc: func() { klog.V(1).InfoS("hello", "what", "one world") },
+ output: "I output.go:] \"hello\" what=\"one world\"\n",
+ },
+ {
+ name: "V().InfoSDepth",
+ logFunc: func() { klog.V(1).InfoSDepth(0, "hello", "what", "one world") },
+ output: "I output.go:] \"hello\" what=\"one world\"\n",
+ },
+ {
+ name: "V().ErrorS",
+ logFunc: func() { klog.V(1).ErrorS(errors.New("hello"), "one world") },
+ output: "E output.go:] \"one world\" err=\"hello\"\n",
+ },
+ }
+ _, _, line, _ := runtime.Caller(0)
+
+ for i, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var buffer bytes.Buffer
+ if config.NewLogger == nil {
+ klog.SetOutput(&buffer)
+ } else {
+ klog.SetLogger(config.NewLogger(&buffer, 10, ""))
+ defer klog.ClearLogger()
+ }
+ test.logFunc()
+ klog.Flush()
+
+ actual := buffer.String()
+ // Strip varying header.
+ re := `(?m)^(.).... ..:..:......... ....... output.go`
+ actual = regexp.MustCompile(re).ReplaceAllString(actual, `${1} output.go`)
+
+ // Inject expected line. This matches the if checks above, which are
+ // the same for both printWithKlog and printWithLogger.
+ callLine := line + 1 - (len(tests)-i)*5
+ expected := test.output
+
+ // When klog does string formating for
+ // non-structured calls, it passes the entire
+ // result, including a trailing newline, to
+ // Logger.Info.
+ if config.NewLogger != nil &&
+ !strings.HasSuffix(test.name, "S") &&
+ !strings.HasSuffix(test.name, "SDepth") {
+ // klog: I output.go:] hello world
+ // with logger: I output.go:] "hello world\n"
+ index := strings.Index(expected, "] ")
+ if index == -1 {
+ t.Fatalf("did not find ] separator: %s", expected)
+ }
+ expected = expected[0:index+2] + strconv.Quote(expected[index+2:]) + "\n"
+
+ // Warnings become info messages.
+ if strings.HasPrefix(expected, "W") {
+ expected = "I" + expected[1:]
+ }
+ }
+
+ if repl, ok := config.ExpectedOutputMapping[expected]; ok {
+ expected = repl
+ }
+ expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", callLine))
+ if actual != expected {
+ t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expected, actual)
+ }
+ })
+ }
+ }
}
func copySlice(in []interface{}) []interface{} {
From 51711a37e60fb7e141eabe4919ae0031778c5822 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Mon, 7 Feb 2022 20:43:52 +0100
Subject: [PATCH 032/125] fix for off-by-one stack unwinding
https://github.com/kubernetes/klog/pull/280/ turned println and printf into
wrapper functions, but the underlying *Depth functions kept passing the same 0
depth to l.output and thus any logr.Logger.
That off-by-one error broke JSON tests in Kubernetes, luckily well before any
of the PR went into a release. It would be nice to have regression testing for
this in klog, but we cannot depend on a Logger which does stack unwinding in
the code, so the only thing that could be done is some kind of GitHub action
which combines klog with some external logger.
The depth=1 in println is also not covered by unit tests, but seems to be
correct based on the JSON tests.
---
klog.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/klog.go b/klog.go
index 7cf60f9ee..96086a429 100644
--- a/klog.go
+++ b/klog.go
@@ -714,7 +714,7 @@ func (l *loggingT) printlnDepth(s severity, logger *logr.Logger, filter LogFilte
args = filter.Filter(args)
}
fmt.Fprintln(buf, args...)
- l.output(s, logger, buf, 0 /* depth */, file, line, false)
+ l.output(s, logger, buf, depth, file, line, false)
}
func (l *loggingT) print(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
@@ -758,7 +758,7 @@ func (l *loggingT) printfDepth(s severity, logger *logr.Logger, filter LogFilter
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
- l.output(s, logger, buf, 0 /* depth */, file, line, false)
+ l.output(s, logger, buf, depth, file, line, false)
}
// printWithFileLine behaves like print but uses the provided file and line number. If
From 5deb0375b928d21265e2818d1b58923552ba1c71 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Thu, 10 Feb 2022 18:22:35 +0100
Subject: [PATCH 033/125] fix new *lnDepth functions
https://github.com/kubernetes/klog/pull/280 added several new functions and
didn't quite get the print vs println difference right: the corresponding
non-Depth functions use fmt.Println and the Depth versions should do the same.
---
klog.go | 8 ++++----
test/output.go | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/klog.go b/klog.go
index 96086a429..26c532b5d 100644
--- a/klog.go
+++ b/klog.go
@@ -1615,7 +1615,7 @@ func Warningln(args ...interface{}) {
// WarninglnDepth acts as Warningln but uses depth to determine which call frame to log.
// WarninglnDepth(0, "msg") is the same as Warningln("msg").
func WarninglnDepth(depth int, args ...interface{}) {
- logging.printDepth(warningLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(warningLog, logging.logr, logging.filter, depth, args...)
}
// Warningf logs to the WARNING and INFO logs.
@@ -1651,7 +1651,7 @@ func Errorln(args ...interface{}) {
// ErrorlnDepth acts as Errorln but uses depth to determine which call frame to log.
// ErrorlnDepth(0, "msg") is the same as Errorln("msg").
func ErrorlnDepth(depth int, args ...interface{}) {
- logging.printDepth(errorLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(errorLog, logging.logr, logging.filter, depth, args...)
}
// Errorf logs to the ERROR, WARNING, and INFO logs.
@@ -1708,7 +1708,7 @@ func Fatalln(args ...interface{}) {
// FatallnDepth acts as Fatalln but uses depth to determine which call frame to log.
// FatallnDepth(0, "msg") is the same as Fatalln("msg").
func FatallnDepth(depth int, args ...interface{}) {
- logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(fatalLog, logging.logr, logging.filter, depth, args...)
}
// Fatalf logs to the FATAL, ERROR, WARNING, and INFO logs,
@@ -1752,7 +1752,7 @@ func Exitln(args ...interface{}) {
// ExitlnDepth(0, "msg") is the same as Exitln("msg").
func ExitlnDepth(depth int, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(fatalLog, logging.logr, logging.filter, depth, args...)
}
// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
diff --git a/test/output.go b/test/output.go
index 0448bea24..a41749140 100644
--- a/test/output.go
+++ b/test/output.go
@@ -499,7 +499,7 @@ I output.go:] "test" firstKey=1 secondKey=3
{
name: "WarninglnDepth",
logFunc: func() { klog.WarninglnDepth(0, "hello", "world") },
- output: "W output.go:] helloworld\n", // BUG
+ output: "W output.go:] hello world\n",
},
{
name: "Warningf",
@@ -529,7 +529,7 @@ I output.go:] "test" firstKey=1 secondKey=3
{
name: "ErrorlnDepth",
logFunc: func() { klog.ErrorlnDepth(0, "hello", "world") },
- output: "E output.go:] helloworld\n", // BUG
+ output: "E output.go:] hello world\n",
},
{
name: "Errorf",
From 08936589e47dd79c5d00c358219693e50a46c700 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 09:27:59 +0100
Subject: [PATCH 034/125] test: benchmark for V
The implementation of V is performance critical: it may get called frequently
on hot code paths and will be responsible for all of the logging overhead when
the log messages then do not need to be formatted.
---
klog_test.go | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/klog_test.go b/klog_test.go
index 17caaf4e0..82f23d8a7 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -623,6 +623,15 @@ func BenchmarkHeaderWithDir(b *testing.B) {
// Ensure that benchmarks have side effects to avoid compiler optimization
var result ObjectRef
+var enabled bool
+
+func BenchmarkV(b *testing.B) {
+ var v Verbose
+ for i := 0; i < b.N; i++ {
+ v = V(10)
+ }
+ enabled = v.Enabled()
+}
func BenchmarkKRef(b *testing.B) {
var r ObjectRef
From ec9f867305d3a6ed08e47cf5d4d4e503f06fc1e5 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 09:31:26 +0100
Subject: [PATCH 035/125] internal: stand-alone implementation of -v and
-verbose
This will be needed for Logger implementations that want to emulate the
traditional verbosity check in klog.
The original code in klog.go intentionally doesn't get modified, for
two reasons:
- avoid the risk of regressions
- an attempt to do so showed that V became slower (extra calls, more
parameters, etc.)
---
internal/verbosity/helper_test.go | 21 ++
internal/verbosity/verbosity.go | 305 +++++++++++++++++++++++++++
internal/verbosity/verbosity_test.go | 97 +++++++++
3 files changed, 423 insertions(+)
create mode 100644 internal/verbosity/helper_test.go
create mode 100644 internal/verbosity/verbosity.go
create mode 100644 internal/verbosity/verbosity_test.go
diff --git a/internal/verbosity/helper_test.go b/internal/verbosity/helper_test.go
new file mode 100644
index 000000000..9ec70c761
--- /dev/null
+++ b/internal/verbosity/helper_test.go
@@ -0,0 +1,21 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package verbosity
+
+func enabledInHelper(vs *VState, l Level) bool {
+ return vs.Enabled(l, 0)
+}
diff --git a/internal/verbosity/verbosity.go b/internal/verbosity/verbosity.go
new file mode 100644
index 000000000..309cdfaa9
--- /dev/null
+++ b/internal/verbosity/verbosity.go
@@ -0,0 +1,305 @@
+/*
+Copyright 2013 Google Inc. All Rights Reserved.
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package verbosity
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+// New returns a struct that implements -v and -vmodule support. Changing and
+// checking these settings is thread-safe, with all concurrency issues handled
+// internally.
+func New() *VState {
+ vs := new(VState)
+
+ // The two fields must have a pointer to the overal struct for their
+ // implementation of Set.
+ vs.vmodule.vs = vs
+ vs.verbosity.vs = vs
+
+ return vs
+}
+
+// Value is an extension that makes it possible to use the values in pflag.
+type Value interface {
+ flag.Value
+ Type() string
+}
+
+func (vs *VState) V() Value {
+ return &vs.verbosity
+}
+
+func (vs *VState) VModule() Value {
+ return &vs.vmodule
+}
+
+// VState contains settings and state. Some of its fields can be accessed
+// through atomic read/writes, in other cases a mutex must be held.
+type VState struct {
+ mu sync.Mutex
+
+ // These flags are modified only under lock, although verbosity may be fetched
+ // safely using atomic.LoadInt32.
+ vmodule moduleSpec // The state of the -vmodule flag.
+ verbosity levelSpec // V logging level, the value of the -v flag/
+
+ // pcs is used in V to avoid an allocation when computing the caller's PC.
+ pcs [1]uintptr
+ // vmap is a cache of the V Level for each V() call site, identified by PC.
+ // It is wiped whenever the vmodule flag changes state.
+ vmap map[uintptr]Level
+ // filterLength stores the length of the vmodule filter chain. If greater
+ // than zero, it means vmodule is enabled. It may be read safely
+ // using sync.LoadInt32, but is only modified under mu.
+ filterLength int32
+}
+
+// Level must be an int32 to support atomic read/writes.
+type Level int32
+
+type levelSpec struct {
+ vs *VState
+ l Level
+}
+
+// get returns the value of the level.
+func (l *levelSpec) get() Level {
+ return Level(atomic.LoadInt32((*int32)(&l.l)))
+}
+
+// set sets the value of the level.
+func (l *levelSpec) set(val Level) {
+ atomic.StoreInt32((*int32)(&l.l), int32(val))
+}
+
+// String is part of the flag.Value interface.
+func (l *levelSpec) String() string {
+ return strconv.FormatInt(int64(l.l), 10)
+}
+
+// Get is part of the flag.Getter interface. It returns the
+// verbosity level as int32.
+func (l *levelSpec) Get() interface{} {
+ return int32(l.l)
+}
+
+// Type is part of pflag.Value.
+func (l *levelSpec) Type() string {
+ return "Level"
+}
+
+// Set is part of the flag.Value interface.
+func (l *levelSpec) Set(value string) error {
+ v, err := strconv.ParseInt(value, 10, 32)
+ if err != nil {
+ return err
+ }
+ l.vs.mu.Lock()
+ defer l.vs.mu.Unlock()
+ l.vs.set(Level(v), l.vs.vmodule.filter, false)
+ return nil
+}
+
+// moduleSpec represents the setting of the -vmodule flag.
+type moduleSpec struct {
+ vs *VState
+ filter []modulePat
+}
+
+// modulePat contains a filter for the -vmodule flag.
+// It holds a verbosity level and a file pattern to match.
+type modulePat struct {
+ pattern string
+ literal bool // The pattern is a literal string
+ level Level
+}
+
+// match reports whether the file matches the pattern. It uses a string
+// comparison if the pattern contains no metacharacters.
+func (m *modulePat) match(file string) bool {
+ if m.literal {
+ return file == m.pattern
+ }
+ match, _ := filepath.Match(m.pattern, file)
+ return match
+}
+
+func (m *moduleSpec) String() string {
+ // Lock because the type is not atomic. TODO: clean this up.
+ // Empty instances don't have and don't need a lock (can
+ // happen when flag uses introspection).
+ if m.vs != nil {
+ m.vs.mu.Lock()
+ defer m.vs.mu.Unlock()
+ }
+ var b bytes.Buffer
+ for i, f := range m.filter {
+ if i > 0 {
+ b.WriteRune(',')
+ }
+ fmt.Fprintf(&b, "%s=%d", f.pattern, f.level)
+ }
+ return b.String()
+}
+
+// Get is part of the (Go 1.2) flag.Getter interface. It always returns nil for this flag type since the
+// struct is not exported.
+func (m *moduleSpec) Get() interface{} {
+ return nil
+}
+
+// Type is part of pflag.Value
+func (m *moduleSpec) Type() string {
+ return "pattern=N,..."
+}
+
+var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of filename=N")
+
+// Set will sets module value
+// Syntax: -vmodule=recordio=2,file=1,gfs*=3
+func (m *moduleSpec) Set(value string) error {
+ var filter []modulePat
+ for _, pat := range strings.Split(value, ",") {
+ if len(pat) == 0 {
+ // Empty strings such as from a trailing comma can be ignored.
+ continue
+ }
+ patLev := strings.Split(pat, "=")
+ if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 {
+ return errVmoduleSyntax
+ }
+ pattern := patLev[0]
+ v, err := strconv.ParseInt(patLev[1], 10, 32)
+ if err != nil {
+ return errors.New("syntax error: expect comma-separated list of filename=N")
+ }
+ if v < 0 {
+ return errors.New("negative value for vmodule level")
+ }
+ if v == 0 {
+ continue // Ignore. It's harmless but no point in paying the overhead.
+ }
+ // TODO: check syntax of filter?
+ filter = append(filter, modulePat{pattern, isLiteral(pattern), Level(v)})
+ }
+ m.vs.mu.Lock()
+ defer m.vs.mu.Unlock()
+ m.vs.set(m.vs.verbosity.l, filter, true)
+ return nil
+}
+
+// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters
+// that require filepath.Match to be called to match the pattern.
+func isLiteral(pattern string) bool {
+ return !strings.ContainsAny(pattern, `\*?[]`)
+}
+
+// set sets a consistent state for V logging.
+// The mutex must be held.
+func (vs *VState) set(l Level, filter []modulePat, setFilter bool) {
+ // Turn verbosity off so V will not fire while we are in transition.
+ vs.verbosity.set(0)
+ // Ditto for filter length.
+ atomic.StoreInt32(&vs.filterLength, 0)
+
+ // Set the new filters and wipe the pc->Level map if the filter has changed.
+ if setFilter {
+ vs.vmodule.filter = filter
+ vs.vmap = make(map[uintptr]Level)
+ }
+
+ // Things are consistent now, so enable filtering and verbosity.
+ // They are enabled in order opposite to that in V.
+ atomic.StoreInt32(&vs.filterLength, int32(len(filter)))
+ vs.verbosity.set(l)
+}
+
+// Enabled checks whether logging is enabled at the given level. This must be
+// called with depth=0 when the caller of enabled will do the logging and
+// higher values when more stack levels need to be skipped.
+//
+// The mutex will be locked only if needed.
+func (vs *VState) Enabled(level Level, depth int) bool {
+ // This function tries hard to be cheap unless there's work to do.
+ // The fast path is two atomic loads and compares.
+
+ // Here is a cheap but safe test to see if V logging is enabled globally.
+ if vs.verbosity.get() >= level {
+ return true
+ }
+
+ // It's off globally but vmodule may still be set.
+ // Here is another cheap but safe test to see if vmodule is enabled.
+ if atomic.LoadInt32(&vs.filterLength) > 0 {
+ // Now we need a proper lock to use the logging structure. The pcs field
+ // is shared so we must lock before accessing it. This is fairly expensive,
+ // but if V logging is enabled we're slow anyway.
+ vs.mu.Lock()
+ defer vs.mu.Unlock()
+ if runtime.Callers(depth+2, vs.pcs[:]) == 0 {
+ return false
+ }
+ // runtime.Callers returns "return PCs", but we want
+ // to look up the symbolic information for the call,
+ // so subtract 1 from the PC. runtime.CallersFrames
+ // would be cleaner, but allocates.
+ pc := vs.pcs[0] - 1
+ v, ok := vs.vmap[pc]
+ if !ok {
+ v = vs.setV(pc)
+ }
+ return v >= level
+ }
+ return false
+}
+
+// setV computes and remembers the V level for a given PC
+// when vmodule is enabled.
+// File pattern matching takes the basename of the file, stripped
+// of its .go suffix, and uses filepath.Match, which is a little more
+// general than the *? matching used in C++.
+// Mutex is held.
+func (vs *VState) setV(pc uintptr) Level {
+ fn := runtime.FuncForPC(pc)
+ file, _ := fn.FileLine(pc)
+ // The file is something like /a/b/c/d.go. We want just the d.
+ if strings.HasSuffix(file, ".go") {
+ file = file[:len(file)-3]
+ }
+ if slash := strings.LastIndex(file, "/"); slash >= 0 {
+ file = file[slash+1:]
+ }
+ for _, filter := range vs.vmodule.filter {
+ if filter.match(file) {
+ vs.vmap[pc] = filter.level
+ return filter.level
+ }
+ }
+ vs.vmap[pc] = 0
+ return 0
+}
diff --git a/internal/verbosity/verbosity_test.go b/internal/verbosity/verbosity_test.go
new file mode 100644
index 000000000..766147b27
--- /dev/null
+++ b/internal/verbosity/verbosity_test.go
@@ -0,0 +1,97 @@
+/*
+Copyright 2013 Google Inc. All Rights Reserved.
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package verbosity
+
+import (
+ "testing"
+)
+
+func TestV(t *testing.T) {
+ vs := New()
+ vs.verbosity.Set("2")
+ depth := 0
+ if !vs.Enabled(1, depth) {
+ t.Error("not enabled for 1")
+ }
+ if !vs.Enabled(2, depth) {
+ t.Error("not enabled for 2")
+ }
+ if vs.Enabled(3, depth) {
+ t.Error("enabled for 3")
+ }
+}
+
+func TestVmoduleOn(t *testing.T) {
+ vs := New()
+ vs.vmodule.Set("verbosity_test=2")
+ depth := 0
+ if !vs.Enabled(1, depth) {
+ t.Error("not enabled for 1")
+ }
+ if !vs.Enabled(2, depth) {
+ t.Error("not enabled for 2")
+ }
+ if vs.Enabled(3, depth) {
+ t.Error("enabled for 3")
+ }
+ if enabledInHelper(vs, 1) {
+ t.Error("enabled for helper at 1")
+ }
+ if enabledInHelper(vs, 2) {
+ t.Error("enabled for helper at 2")
+ }
+ if enabledInHelper(vs, 3) {
+ t.Error("enabled for helper at 3")
+ }
+}
+
+// vGlobs are patterns that match/don't match this file at V=2.
+var vGlobs = map[string]bool{
+ // Easy to test the numeric match here.
+ "verbosity_test=1": false, // If -vmodule sets V to 1, V(2) will fail.
+ "verbosity_test=2": true,
+ "verbosity_test=3": true, // If -vmodule sets V to 1, V(3) will succeed.
+ // These all use 2 and check the patterns. All are true.
+ "*=2": true,
+ "?e*=2": true,
+ "?????????_*=2": true,
+ "??[arx]??????_*t=2": true,
+ // These all use 2 and check the patterns. All are false.
+ "*x=2": false,
+ "m*=2": false,
+ "??_*=2": false,
+ "?[abc]?_*t=2": false,
+}
+
+// Test that vmodule globbing works as advertised.
+func testVmoduleGlob(pat string, match bool, t *testing.T) {
+ vs := New()
+ vs.vmodule.Set(pat)
+ depth := 0
+ actual := vs.Enabled(2, depth)
+ if actual != match {
+ t.Errorf("incorrect match for %q: got %#v expected %#v", pat, actual, match)
+ }
+}
+
+// Test that a vmodule globbing works as advertised.
+func TestVmoduleGlob(t *testing.T) {
+ for glob, match := range vGlobs {
+ testVmoduleGlob(glob, match, t)
+ }
+}
From 69f61114bd90486903459b6892e4985d2e24b0bc Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 09:37:51 +0100
Subject: [PATCH 036/125] internal: move key/value handling into stand-alone
package
This makes it possible to reuse the klog output formatting in logr.Logger
implementations.
---
internal/serialize/keyvalues.go | 185 +++++++++++++++++++++++++++
internal/serialize/keyvalues_test.go | 151 ++++++++++++++++++++++
internal/test/mock.go | 40 ++++++
klog.go | 121 +-----------------
klog_test.go | 175 +++----------------------
klogr/klogr.go | 60 ++-------
6 files changed, 405 insertions(+), 327 deletions(-)
create mode 100644 internal/serialize/keyvalues.go
create mode 100644 internal/serialize/keyvalues_test.go
create mode 100644 internal/test/mock.go
diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go
new file mode 100644
index 000000000..479a37351
--- /dev/null
+++ b/internal/serialize/keyvalues.go
@@ -0,0 +1,185 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package serialize
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+)
+
+// TrimDuplicates deduplicates elements provided in multiple key/value tuple
+// slices, whilst maintaining the distinction between where the items are
+// contained.
+func TrimDuplicates(kvLists ...[]interface{}) [][]interface{} {
+ // maintain a map of all seen keys
+ seenKeys := map[interface{}]struct{}{}
+ // build the same number of output slices as inputs
+ outs := make([][]interface{}, len(kvLists))
+ // iterate over the input slices backwards, as 'later' kv specifications
+ // of the same key will take precedence over earlier ones
+ for i := len(kvLists) - 1; i >= 0; i-- {
+ // initialise this output slice
+ outs[i] = []interface{}{}
+ // obtain a reference to the kvList we are processing
+ kvList := kvLists[i]
+
+ // start iterating at len(kvList) - 2 (i.e. the 2nd last item) for
+ // slices that have an even number of elements.
+ // We add (len(kvList) % 2) here to handle the case where there is an
+ // odd number of elements in a kvList.
+ // If there is an odd number, then the last element in the slice will
+ // have the value 'null'.
+ for i2 := len(kvList) - 2 + (len(kvList) % 2); i2 >= 0; i2 -= 2 {
+ k := kvList[i2]
+ // if we have already seen this key, do not include it again
+ if _, ok := seenKeys[k]; ok {
+ continue
+ }
+ // make a note that we've observed a new key
+ seenKeys[k] = struct{}{}
+ // attempt to obtain the value of the key
+ var v interface{}
+ // i2+1 should only ever be out of bounds if we handling the first
+ // iteration over a slice with an odd number of elements
+ if i2+1 < len(kvList) {
+ v = kvList[i2+1]
+ }
+ // add this KV tuple to the *start* of the output list to maintain
+ // the original order as we are iterating over the slice backwards
+ outs[i] = append([]interface{}{k, v}, outs[i]...)
+ }
+ }
+ return outs
+}
+
+const missingValue = "(MISSING)"
+
+// KVListFormat serializes all key/value pairs into the provided buffer.
+// A space gets inserted before the first pair and between each pair.
+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(' ')
+ // Keys are assumed to be well-formed according to
+ // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
+ // for the sake of performance. Keys with spaces,
+ // special characters, etc. will break parsing.
+ if k, ok := k.(string); ok {
+ // Avoid one allocation when the key is a string, which
+ // normally it should be.
+ b.WriteString(k)
+ } else {
+ b.WriteString(fmt.Sprintf("%s", k))
+ }
+
+ // The type checks are sorted so that more frequently used ones
+ // come first because that is then faster in the common
+ // cases. In Kubernetes, ObjectRef (a Stringer) is more common
+ // than plain strings
+ // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
+ switch v := v.(type) {
+ case fmt.Stringer:
+ writeStringValue(b, true, stringerToString(v))
+ case string:
+ writeStringValue(b, true, v)
+ case error:
+ writeStringValue(b, true, v.Error())
+ case []byte:
+ // In https://github.com/kubernetes/klog/pull/237 it was decided
+ // to format byte slices with "%+q". The advantages of that are:
+ // - readable output if the bytes happen to be printable
+ // - non-printable bytes get represented as unicode escape
+ // sequences (\uxxxx)
+ //
+ // The downsides are that we cannot use the faster
+ // strconv.Quote here and that multi-line output is not
+ // supported. If developers know that a byte array is
+ // printable and they want multi-line output, they can
+ // convert the value to string before logging it.
+ b.WriteByte('=')
+ b.WriteString(fmt.Sprintf("%+q", v))
+ default:
+ writeStringValue(b, false, fmt.Sprintf("%+v", v))
+ }
+ }
+}
+
+func stringerToString(s fmt.Stringer) (ret string) {
+ defer func() {
+ if err := recover(); err != nil {
+ ret = "nil"
+ }
+ }()
+ ret = s.String()
+ return
+}
+
+func writeStringValue(b *bytes.Buffer, quote bool, v string) {
+ data := []byte(v)
+ index := bytes.IndexByte(data, '\n')
+ if index == -1 {
+ b.WriteByte('=')
+ if quote {
+ // Simple string, quote quotation marks and non-printable characters.
+ b.WriteString(strconv.Quote(v))
+ return
+ }
+ // Non-string with no line breaks.
+ b.WriteString(v)
+ return
+ }
+
+ // Complex multi-line string, show as-is with indention like this:
+ // I... "hello world" key=<
+ // line 1
+ // line 2
+ // >
+ //
+ // Tabs indent the lines of the value while the end of string delimiter
+ // is indented with a space. That has two purposes:
+ // - visual difference between the two for a human reader because indention
+ // will be different
+ // - no ambiguity when some value line starts with the end delimiter
+ //
+ // One downside is that the output cannot distinguish between strings that
+ // end with a line break and those that don't because the end delimiter
+ // will always be on the next line.
+ b.WriteString("=<\n")
+ for index != -1 {
+ b.WriteByte('\t')
+ b.Write(data[0 : index+1])
+ data = data[index+1:]
+ index = bytes.IndexByte(data, '\n')
+ }
+ if len(data) == 0 {
+ // String ended with line break, don't add another.
+ b.WriteString(" >")
+ } else {
+ // No line break at end of last line, write rest of string and
+ // add one.
+ b.WriteByte('\t')
+ b.Write(data)
+ b.WriteString("\n >")
+ }
+}
diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go
new file mode 100644
index 000000000..f7fdaba36
--- /dev/null
+++ b/internal/serialize/keyvalues_test.go
@@ -0,0 +1,151 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package serialize_test
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+ "time"
+
+ "k8s.io/klog/v2"
+ "k8s.io/klog/v2/internal/serialize"
+ "k8s.io/klog/v2/internal/test"
+)
+
+// point conforms to fmt.Stringer interface as it implements the String() method
+type point struct {
+ x int
+ y int
+}
+
+// we now have a value receiver
+func (p point) String() string {
+ return fmt.Sprintf("x=%d, y=%d", p.x, p.y)
+}
+
+// Test that kvListFormat works as advertised.
+func TestKvListFormat(t *testing.T) {
+ var emptyPoint *point
+ 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
+ N time.Time
+ }{X: 76, Y: "strval", N: time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.UTC)}},
+ want: " pod=\"kubedns\" spec={X:76 Y:strval N:2006-01-02 15:04:05.06789 +0000 UTC}",
+ },
+ {
+ keysValues: []interface{}{"pod", "kubedns", "values", []int{8, 6, 7, 5, 3, 0, 9}},
+ want: " pod=\"kubedns\" values=[8 6 7 5 3 0 9]",
+ },
+ {
+ keysValues: []interface{}{"pod", "kubedns", "values", []string{"deployment", "svc", "configmap"}},
+ want: " pod=\"kubedns\" values=[deployment svc configmap]",
+ },
+ {
+ keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("test case for byte array")},
+ want: " pod=\"kubedns\" bytes=\"test case for byte array\"",
+ },
+ {
+ keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("��=� ⌘")},
+ want: " pod=\"kubedns\" bytes=\"\\ufffd\\ufffd=\\ufffd \\u2318\"",
+ },
+ {
+ keysValues: []interface{}{"multiLineString", `Hello world!
+ Starts with tab.
+ Starts with spaces.
+No whitespace.`,
+ "pod", "kubedns",
+ },
+ want: ` multiLineString=<
+ Hello world!
+ Starts with tab.
+ Starts with spaces.
+ No whitespace.
+ > pod="kubedns"`,
+ },
+ {
+ keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}},
+ want: " pod=\"kubedns\" maps=map[three:4]",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KRef("kube-system", "kubedns"), "status", "ready"},
+ want: " pod=\"kube-system/kubedns\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KRef("", "kubedns"), "status", "ready"},
+ want: " pod=\"kubedns\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KObj(test.KMetadataMock{Name: "test-name", NS: "test-ns"}), "status", "ready"},
+ want: " pod=\"test-ns/test-name\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KObj(test.KMetadataMock{Name: "test-name", NS: ""}), "status", "ready"},
+ want: " pod=\"test-name\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KObj(nil), "status", "ready"},
+ want: " pod=\"\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KObj((*test.PtrKMetadataMock)(nil)), "status", "ready"},
+ want: " pod=\"\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pod", klog.KObj((*test.KMetadataMock)(nil)), "status", "ready"},
+ want: " pod=\"\" status=\"ready\"",
+ },
+ {
+ keysValues: []interface{}{"pods", klog.KObjs([]test.KMetadataMock{
+ {
+ Name: "kube-dns",
+ NS: "kube-system",
+ },
+ {
+ Name: "mi-conf",
+ },
+ })},
+ want: " pods=[kube-system/kube-dns mi-conf]",
+ },
+ {
+ keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint},
+ want: " point-1=\"x=100, y=200\" point-2=\"nil\"",
+ },
+ }
+
+ for _, d := range testKVList {
+ b := &bytes.Buffer{}
+ serialize.KVListFormat(b, d.keysValues...)
+ if b.String() != d.want {
+ t.Errorf("KVListFormat error:\n got:\n\t%s\nwant:\t%s", b.String(), d.want)
+ }
+ }
+}
diff --git a/internal/test/mock.go b/internal/test/mock.go
new file mode 100644
index 000000000..3f5a6eaae
--- /dev/null
+++ b/internal/test/mock.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package test contains common code for klog tests.
+package test
+
+type KMetadataMock struct {
+ Name, NS string
+}
+
+func (m KMetadataMock) GetName() string {
+ return m.Name
+}
+func (m KMetadataMock) GetNamespace() string {
+ return m.NS
+}
+
+type PtrKMetadataMock struct {
+ Name, NS string
+}
+
+func (m *PtrKMetadataMock) GetName() string {
+ return m.Name
+}
+func (m *PtrKMetadataMock) GetNamespace() string {
+ return m.NS
+}
diff --git a/klog.go b/klog.go
index 26c532b5d..7e7865744 100644
--- a/klog.go
+++ b/klog.go
@@ -90,6 +90,8 @@ import (
"time"
"github.com/go-logr/logr"
+
+ "k8s.io/klog/v2/internal/serialize"
)
// severity identifies the sort of log: info, warning etc. It also implements
@@ -816,129 +818,14 @@ func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndV
// message and put the multi-line output into a value.
b.WriteString(strconv.Quote(msg))
if err != nil {
- kvListFormat(&b.Buffer, "err", err)
+ serialize.KVListFormat(&b.Buffer, "err", err)
}
- kvListFormat(&b.Buffer, keysAndValues...)
+ serialize.KVListFormat(&b.Buffer, keysAndValues...)
l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer)
// Make the buffer available for reuse.
l.putBuffer(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(' ')
- // Keys are assumed to be well-formed according to
- // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
- // for the sake of performance. Keys with spaces,
- // special characters, etc. will break parsing.
- if k, ok := k.(string); ok {
- // Avoid one allocation when the key is a string, which
- // normally it should be.
- b.WriteString(k)
- } else {
- b.WriteString(fmt.Sprintf("%s", k))
- }
-
- // The type checks are sorted so that more frequently used ones
- // come first because that is then faster in the common
- // cases. In Kubernetes, ObjectRef (a Stringer) is more common
- // than plain strings
- // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
- switch v := v.(type) {
- case fmt.Stringer:
- writeStringValue(b, true, stringerToString(v))
- case string:
- writeStringValue(b, true, v)
- case error:
- writeStringValue(b, true, v.Error())
- case []byte:
- // In https://github.com/kubernetes/klog/pull/237 it was decided
- // to format byte slices with "%+q". The advantages of that are:
- // - readable output if the bytes happen to be printable
- // - non-printable bytes get represented as unicode escape
- // sequences (\uxxxx)
- //
- // The downsides are that we cannot use the faster
- // strconv.Quote here and that multi-line output is not
- // supported. If developers know that a byte array is
- // printable and they want multi-line output, they can
- // convert the value to string before logging it.
- b.WriteByte('=')
- b.WriteString(fmt.Sprintf("%+q", v))
- default:
- writeStringValue(b, false, fmt.Sprintf("%+v", v))
- }
- }
-}
-
-func stringerToString(s fmt.Stringer) (ret string) {
- defer func() {
- if err := recover(); err != nil {
- ret = "nil"
- }
- }()
- ret = s.String()
- return
-}
-
-func writeStringValue(b *bytes.Buffer, quote bool, v string) {
- data := []byte(v)
- index := bytes.IndexByte(data, '\n')
- if index == -1 {
- b.WriteByte('=')
- if quote {
- // Simple string, quote quotation marks and non-printable characters.
- b.WriteString(strconv.Quote(v))
- return
- }
- // Non-string with no line breaks.
- b.WriteString(v)
- return
- }
-
- // Complex multi-line string, show as-is with indention like this:
- // I... "hello world" key=<
- // line 1
- // line 2
- // >
- //
- // Tabs indent the lines of the value while the end of string delimiter
- // is indented with a space. That has two purposes:
- // - visual difference between the two for a human reader because indention
- // will be different
- // - no ambiguity when some value line starts with the end delimiter
- //
- // One downside is that the output cannot distinguish between strings that
- // end with a line break and those that don't because the end delimiter
- // will always be on the next line.
- b.WriteString("=<\n")
- for index != -1 {
- b.WriteByte('\t')
- b.Write(data[0 : index+1])
- data = data[index+1:]
- index = bytes.IndexByte(data, '\n')
- }
- if len(data) == 0 {
- // String ended with line break, don't add another.
- b.WriteString(" >")
- } else {
- // No line break at end of last line, write rest of string and
- // add one.
- b.WriteByte('\t')
- b.Write(data)
- b.WriteString("\n >")
- }
-}
-
// redirectBuffer is used to set an alternate destination for the logs
type redirectBuffer struct {
w io.Writer
diff --git a/klog_test.go b/klog_test.go
index 82f23d8a7..f4bcc7764 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -35,6 +35,8 @@ import (
"time"
"github.com/go-logr/logr"
+
+ "k8s.io/klog/v2/internal/test"
)
// TODO: This test package should be refactored so that tests cannot
@@ -642,7 +644,7 @@ func BenchmarkKRef(b *testing.B) {
}
func BenchmarkKObj(b *testing.B) {
- a := kMetadataMock{name: "a", ns: "a"}
+ a := test.KMetadataMock{Name: "a", NS: "a"}
var r ObjectRef
for i := 0; i < b.N; i++ {
r = KObj(&a)
@@ -780,28 +782,6 @@ func TestInfoObjectRef(t *testing.T) {
}
}
-type kMetadataMock struct {
- name, ns string
-}
-
-func (m kMetadataMock) GetName() string {
- return m.name
-}
-func (m kMetadataMock) GetNamespace() string {
- return m.ns
-}
-
-type ptrKMetadataMock struct {
- name, ns string
-}
-
-func (m *ptrKMetadataMock) GetName() string {
- return m.name
-}
-func (m *ptrKMetadataMock) GetNamespace() string {
- return m.ns
-}
-
func TestKObj(t *testing.T) {
tests := []struct {
name string
@@ -810,17 +790,17 @@ func TestKObj(t *testing.T) {
}{
{
name: "nil passed as pointer KMetadata implementation",
- obj: (*ptrKMetadataMock)(nil),
+ obj: (*test.PtrKMetadataMock)(nil),
want: ObjectRef{},
},
{
name: "empty struct passed as non-pointer KMetadata implementation",
- obj: kMetadataMock{},
+ obj: test.KMetadataMock{},
want: ObjectRef{},
},
{
name: "nil pointer passed to non-pointer KMetadata implementation",
- obj: (*kMetadataMock)(nil),
+ obj: (*test.KMetadataMock)(nil),
want: ObjectRef{},
},
{
@@ -830,7 +810,7 @@ func TestKObj(t *testing.T) {
},
{
name: "with ns",
- obj: &kMetadataMock{"test-name", "test-ns"},
+ obj: &test.KMetadataMock{Name: "test-name", NS: "test-ns"},
want: ObjectRef{
Name: "test-name",
Namespace: "test-ns",
@@ -838,7 +818,7 @@ func TestKObj(t *testing.T) {
},
{
name: "without ns",
- obj: &kMetadataMock{"test-name", ""},
+ obj: &test.KMetadataMock{Name: "test-name", NS: ""},
want: ObjectRef{
Name: "test-name",
},
@@ -1072,129 +1052,6 @@ func TestErrorS(t *testing.T) {
}
}
-// point conforms to fmt.Stringer interface as it implements the String() method
-type point struct {
- x int
- y int
-}
-
-// we now have a value receiver
-func (p point) String() string {
- return fmt.Sprintf("x=%d, y=%d", p.x, p.y)
-}
-
-// Test that kvListFormat works as advertised.
-func TestKvListFormat(t *testing.T) {
- var emptyPoint *point
- 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
- N time.Time
- }{X: 76, Y: "strval", N: time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.UTC)}},
- want: " pod=\"kubedns\" spec={X:76 Y:strval N:2006-01-02 15:04:05.06789 +0000 UTC}",
- },
- {
- keysValues: []interface{}{"pod", "kubedns", "values", []int{8, 6, 7, 5, 3, 0, 9}},
- want: " pod=\"kubedns\" values=[8 6 7 5 3 0 9]",
- },
- {
- keysValues: []interface{}{"pod", "kubedns", "values", []string{"deployment", "svc", "configmap"}},
- want: " pod=\"kubedns\" values=[deployment svc configmap]",
- },
- {
- keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("test case for byte array")},
- want: " pod=\"kubedns\" bytes=\"test case for byte array\"",
- },
- {
- keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("��=� ⌘")},
- want: " pod=\"kubedns\" bytes=\"\\ufffd\\ufffd=\\ufffd \\u2318\"",
- },
- {
- keysValues: []interface{}{"multiLineString", `Hello world!
- Starts with tab.
- Starts with spaces.
-No whitespace.`,
- "pod", "kubedns",
- },
- want: ` multiLineString=<
- Hello world!
- Starts with tab.
- Starts with spaces.
- No whitespace.
- > pod="kubedns"`,
- },
- {
- keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}},
- want: " pod=\"kubedns\" maps=map[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(kMetadataMock{"test-name", "test-ns"}), "status", "ready"},
- want: " pod=\"test-ns/test-name\" status=\"ready\"",
- },
- {
- keysValues: []interface{}{"pod", KObj(kMetadataMock{"test-name", ""}), "status", "ready"},
- want: " pod=\"test-name\" status=\"ready\"",
- },
- {
- keysValues: []interface{}{"pod", KObj(nil), "status", "ready"},
- want: " pod=\"\" status=\"ready\"",
- },
- {
- keysValues: []interface{}{"pod", KObj((*ptrKMetadataMock)(nil)), "status", "ready"},
- want: " pod=\"\" status=\"ready\"",
- },
- {
- keysValues: []interface{}{"pod", KObj((*kMetadataMock)(nil)), "status", "ready"},
- want: " pod=\"\" status=\"ready\"",
- },
- {
- keysValues: []interface{}{"pods", KObjs([]kMetadataMock{
- {
- name: "kube-dns",
- ns: "kube-system",
- },
- {
- name: "mi-conf",
- },
- })},
- want: " pods=[kube-system/kube-dns mi-conf]",
- },
- {
- keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint},
- want: " point-1=\"x=100, y=200\" point-2=\"nil\"",
- },
- }
-
- for _, d := range testKVList {
- b := &bytes.Buffer{}
- kvListFormat(b, d.keysValues...)
- if b.String() != d.want {
- t.Errorf("kvlist format error:\ngot:\n%s\nwant:\n%s\n", b.String(), d.want)
- }
- }
-}
-
func createTestValueOfLoggingT() *loggingT {
l := new(loggingT)
l.toStderr = true
@@ -1888,13 +1745,13 @@ func TestKObjs(t *testing.T) {
}{
{
name: "test for KObjs function with KMetadata slice",
- obj: []kMetadataMock{
+ obj: []test.KMetadataMock{
{
- name: "kube-dns",
- ns: "kube-system",
+ Name: "kube-dns",
+ NS: "kube-system",
},
{
- name: "mi-conf",
+ Name: "mi-conf",
},
{},
},
@@ -1911,13 +1768,13 @@ func TestKObjs(t *testing.T) {
},
{
name: "test for KObjs function with KMetadata pointer slice",
- obj: []*kMetadataMock{
+ obj: []*test.KMetadataMock{
{
- name: "kube-dns",
- ns: "kube-system",
+ Name: "kube-dns",
+ NS: "kube-system",
},
{
- name: "mi-conf",
+ Name: "mi-conf",
},
nil,
},
diff --git a/klogr/klogr.go b/klogr/klogr.go
index a347a8849..d8e660e45 100644
--- a/klogr/klogr.go
+++ b/klogr/klogr.go
@@ -6,10 +6,13 @@ import (
"bytes"
"encoding/json"
"fmt"
- "github.com/go-logr/logr"
- "k8s.io/klog/v2"
"sort"
"strings"
+
+ "github.com/go-logr/logr"
+
+ "k8s.io/klog/v2"
+ "k8s.io/klog/v2/internal/serialize"
)
// Option is a functional option that reconfigures the logger created with New.
@@ -70,51 +73,6 @@ func (l *klogger) Init(info logr.RuntimeInfo) {
l.callDepth += info.CallDepth
}
-// trimDuplicates will deduplicate elements provided in multiple KV tuple
-// slices, whilst maintaining the distinction between where the items are
-// contained.
-func trimDuplicates(kvLists ...[]interface{}) [][]interface{} {
- // maintain a map of all seen keys
- seenKeys := map[interface{}]struct{}{}
- // build the same number of output slices as inputs
- outs := make([][]interface{}, len(kvLists))
- // iterate over the input slices backwards, as 'later' kv specifications
- // of the same key will take precedence over earlier ones
- for i := len(kvLists) - 1; i >= 0; i-- {
- // initialise this output slice
- outs[i] = []interface{}{}
- // obtain a reference to the kvList we are processing
- kvList := kvLists[i]
-
- // start iterating at len(kvList) - 2 (i.e. the 2nd last item) for
- // slices that have an even number of elements.
- // We add (len(kvList) % 2) here to handle the case where there is an
- // odd number of elements in a kvList.
- // If there is an odd number, then the last element in the slice will
- // have the value 'null'.
- for i2 := len(kvList) - 2 + (len(kvList) % 2); i2 >= 0; i2 -= 2 {
- k := kvList[i2]
- // if we have already seen this key, do not include it again
- if _, ok := seenKeys[k]; ok {
- continue
- }
- // make a note that we've observed a new key
- seenKeys[k] = struct{}{}
- // attempt to obtain the value of the key
- var v interface{}
- // i2+1 should only ever be out of bounds if we handling the first
- // iteration over a slice with an odd number of elements
- if i2+1 < len(kvList) {
- v = kvList[i2+1]
- }
- // add this KV tuple to the *start* of the output list to maintain
- // the original order as we are iterating over the slice backwards
- outs[i] = append([]interface{}{k, v}, outs[i]...)
- }
- }
- return outs
-}
-
func flatten(kvList ...interface{}) string {
keys := make([]string, 0, len(kvList))
vals := make(map[string]interface{}, len(kvList))
@@ -161,12 +119,12 @@ func (l klogger) Info(level int, msg string, kvList ...interface{}) {
switch l.format {
case FormatSerialize:
msgStr := flatten("msg", msg)
- trimmed := trimDuplicates(l.values, kvList)
+ trimmed := serialize.TrimDuplicates(l.values, kvList)
fixedStr := flatten(trimmed[0]...)
userStr := flatten(trimmed[1]...)
klog.InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr)
case FormatKlog:
- trimmed := trimDuplicates(l.values, kvList)
+ trimmed := serialize.TrimDuplicates(l.values, kvList)
if l.prefix != "" {
msg = l.prefix + ": " + msg
}
@@ -187,12 +145,12 @@ func (l klogger) Error(err error, msg string, kvList ...interface{}) {
switch l.format {
case FormatSerialize:
errStr := flatten("error", loggableErr)
- trimmed := trimDuplicates(l.values, kvList)
+ trimmed := serialize.TrimDuplicates(l.values, kvList)
fixedStr := flatten(trimmed[0]...)
userStr := flatten(trimmed[1]...)
klog.ErrorDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", errStr, " ", fixedStr, " ", userStr)
case FormatKlog:
- trimmed := trimDuplicates(l.values, kvList)
+ trimmed := serialize.TrimDuplicates(l.values, kvList)
if l.prefix != "" {
msg = l.prefix + ": " + msg
}
From c734dc7685cf34c8e3dad5ddaf7d3cc48c80d49e Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 16:50:10 +0100
Subject: [PATCH 037/125] split out buffer helpers and severity definition
This will enable writing Logger implementations which reproduce the traditional
klog header.
---
internal/buffer/buffer.go | 159 +++++++++++++++
internal/severity/severity.go | 58 ++++++
klog.go | 367 +++++++++++-----------------------
klog_test.go | 226 +++++++++++----------
4 files changed, 449 insertions(+), 361 deletions(-)
create mode 100644 internal/buffer/buffer.go
create mode 100644 internal/severity/severity.go
diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go
new file mode 100644
index 000000000..ac88682a2
--- /dev/null
+++ b/internal/buffer/buffer.go
@@ -0,0 +1,159 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+// Copyright 2022 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package buffer provides a cache for byte.Buffer instances that can be reused
+// to avoid frequent allocation and deallocation. It also has utility code
+// for log header formatting that use these buffers.
+package buffer
+
+import (
+ "bytes"
+ "os"
+ "sync"
+ "time"
+
+ "k8s.io/klog/v2/internal/severity"
+)
+
+var (
+ // Pid is inserted into log headers. Can be overridden for tests.
+ Pid = os.Getpid()
+)
+
+// Buffer holds a single byte.Buffer for reuse. The zero value is ready for
+// use. It also provides some helper methods for output formatting.
+type Buffer struct {
+ bytes.Buffer
+ Tmp [64]byte // temporary byte array for creating headers.
+ next *Buffer
+}
+
+// Buffers manages the reuse of individual buffer instances. It is thread-safe.
+type Buffers struct {
+ // mu protects the free list. It is separate from the main mutex
+ // so buffers can be grabbed and printed to without holding the main lock,
+ // for better parallelization.
+ mu sync.Mutex
+
+ // freeList is a list of byte buffers, maintained under mu.
+ freeList *Buffer
+}
+
+// GetBuffer returns a new, ready-to-use buffer.
+func (bl *Buffers) GetBuffer() *Buffer {
+ bl.mu.Lock()
+ b := bl.freeList
+ if b != nil {
+ bl.freeList = b.next
+ }
+ bl.mu.Unlock()
+ if b == nil {
+ b = new(Buffer)
+ } else {
+ b.next = nil
+ b.Reset()
+ }
+ return b
+}
+
+// PutBuffer returns a buffer to the free list.
+func (bl *Buffers) PutBuffer(b *Buffer) {
+ if b.Len() >= 256 {
+ // Let big buffers die a natural death.
+ return
+ }
+ bl.mu.Lock()
+ b.next = bl.freeList
+ bl.freeList = b
+ bl.mu.Unlock()
+}
+
+// Some custom tiny helper functions to print the log header efficiently.
+
+const digits = "0123456789"
+
+// twoDigits formats a zero-prefixed two-digit integer at buf.Tmp[i].
+func (buf *Buffer) twoDigits(i, d int) {
+ buf.Tmp[i+1] = digits[d%10]
+ d /= 10
+ buf.Tmp[i] = digits[d%10]
+}
+
+// nDigits formats an n-digit integer at buf.Tmp[i],
+// padding with pad on the left.
+// It assumes d >= 0.
+func (buf *Buffer) nDigits(n, i, d int, pad byte) {
+ j := n - 1
+ for ; j >= 0 && d > 0; j-- {
+ buf.Tmp[i+j] = digits[d%10]
+ d /= 10
+ }
+ for ; j >= 0; j-- {
+ buf.Tmp[i+j] = pad
+ }
+}
+
+// someDigits formats a zero-prefixed variable-width integer at buf.Tmp[i].
+func (buf *Buffer) someDigits(i, d int) int {
+ // Print into the top, then copy down. We know there's space for at least
+ // a 10-digit number.
+ j := len(buf.Tmp)
+ for {
+ j--
+ buf.Tmp[j] = digits[d%10]
+ d /= 10
+ if d == 0 {
+ break
+ }
+ }
+ return copy(buf.Tmp[i:], buf.Tmp[j:])
+}
+
+// FormatHeader formats a log header using the provided file name and line number.
+func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now time.Time) {
+ if line < 0 {
+ line = 0 // not a real line number, but acceptable to someDigits
+ }
+ if s > severity.FatalLog {
+ s = severity.InfoLog // for safety.
+ }
+
+ // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand.
+ // It's worth about 3X. Fprintf is hard.
+ _, month, day := now.Date()
+ hour, minute, second := now.Clock()
+ // Lmmdd hh:mm:ss.uuuuuu threadid file:line]
+ buf.Tmp[0] = severity.Char[s]
+ buf.twoDigits(1, int(month))
+ buf.twoDigits(3, day)
+ buf.Tmp[5] = ' '
+ buf.twoDigits(6, hour)
+ buf.Tmp[8] = ':'
+ buf.twoDigits(9, minute)
+ buf.Tmp[11] = ':'
+ buf.twoDigits(12, second)
+ buf.Tmp[14] = '.'
+ buf.nDigits(6, 15, now.Nanosecond()/1000, '0')
+ buf.Tmp[21] = ' '
+ buf.nDigits(7, 22, Pid, ' ') // TODO: should be TID
+ buf.Tmp[29] = ' '
+ buf.Write(buf.Tmp[:30])
+ buf.WriteString(file)
+ buf.Tmp[0] = ':'
+ n := buf.someDigits(1, line)
+ buf.Tmp[n+1] = ']'
+ buf.Tmp[n+2] = ' '
+ buf.Write(buf.Tmp[:n+3])
+}
diff --git a/internal/severity/severity.go b/internal/severity/severity.go
new file mode 100644
index 000000000..30fa1834f
--- /dev/null
+++ b/internal/severity/severity.go
@@ -0,0 +1,58 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+// Copyright 2022 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package severity provides definitions for klog severity (info, warning, ...)
+package severity
+
+import (
+ "strings"
+)
+
+// severity identifies the sort of log: info, warning etc. The binding to flag.Value
+// is handled in klog.go
+type Severity int32 // sync/atomic int32
+
+// These constants identify the log levels in order of increasing severity.
+// A message written to a high-severity log file is also written to each
+// lower-severity log file.
+const (
+ InfoLog Severity = iota
+ WarningLog
+ ErrorLog
+ FatalLog
+ NumSeverity = 4
+)
+
+// Char contains one shortcut letter per severity level.
+const Char = "IWEF"
+
+// Name contains one name per severity level.
+var Name = []string{
+ InfoLog: "INFO",
+ WarningLog: "WARNING",
+ ErrorLog: "ERROR",
+ FatalLog: "FATAL",
+}
+
+// ByName looks up a severity level by name.
+func ByName(s string) (Severity, bool) {
+ s = strings.ToUpper(s)
+ for i, name := range Name {
+ if name == s {
+ return Severity(i), true
+ }
+ }
+ return 0, false
+}
diff --git a/klog.go b/klog.go
index 7e7865744..3435461d8 100644
--- a/klog.go
+++ b/klog.go
@@ -91,82 +91,56 @@ import (
"github.com/go-logr/logr"
+ "k8s.io/klog/v2/internal/buffer"
"k8s.io/klog/v2/internal/serialize"
+ "k8s.io/klog/v2/internal/severity"
)
-// severity identifies the sort of log: info, warning etc. It also implements
+// severityValue identifies the sort of log: info, warning etc. It also implements
// the flag.Value interface. The -stderrthreshold flag is of type severity and
// should be modified only through the flag.Value interface. The values match
// the corresponding constants in C++.
-type severity int32 // sync/atomic int32
-
-// These constants identify the log levels in order of increasing severity.
-// A message written to a high-severity log file is also written to each
-// lower-severity log file.
-const (
- infoLog severity = iota
- warningLog
- errorLog
- fatalLog
- numSeverity = 4
-)
-
-const severityChar = "IWEF"
-
-var severityName = []string{
- infoLog: "INFO",
- warningLog: "WARNING",
- errorLog: "ERROR",
- fatalLog: "FATAL",
+type severityValue struct {
+ severity.Severity
}
// get returns the value of the severity.
-func (s *severity) get() severity {
- return severity(atomic.LoadInt32((*int32)(s)))
+func (s *severityValue) get() severity.Severity {
+ return severity.Severity(atomic.LoadInt32((*int32)(&s.Severity)))
}
// set sets the value of the severity.
-func (s *severity) set(val severity) {
- atomic.StoreInt32((*int32)(s), int32(val))
+func (s *severityValue) set(val severity.Severity) {
+ atomic.StoreInt32((*int32)(&s.Severity), int32(val))
}
// String is part of the flag.Value interface.
-func (s *severity) String() string {
- return strconv.FormatInt(int64(*s), 10)
+func (s *severityValue) String() string {
+ return strconv.FormatInt(int64(s.Severity), 10)
}
// Get is part of the flag.Getter interface.
-func (s *severity) Get() interface{} {
- return *s
+func (s *severityValue) Get() interface{} {
+ return s.Severity
}
// Set is part of the flag.Value interface.
-func (s *severity) Set(value string) error {
- var threshold severity
+func (s *severityValue) Set(value string) error {
+ var threshold severity.Severity
// Is it a known name?
- if v, ok := severityByName(value); ok {
+ if v, ok := severity.ByName(value); ok {
threshold = v
} else {
v, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return err
}
- threshold = severity(v)
+ threshold = severity.Severity(v)
}
logging.stderrThreshold.set(threshold)
return nil
}
-func severityByName(s string) (severity, bool) {
- s = strings.ToUpper(s)
- for i, name := range severityName {
- if name == s {
- return severity(i), true
- }
- }
- return 0, false
-}
-
// OutputStats tracks the number of output lines and bytes written.
type OutputStats struct {
lines int64
@@ -189,10 +163,10 @@ var Stats struct {
Info, Warning, Error OutputStats
}
-var severityStats = [numSeverity]*OutputStats{
- infoLog: &Stats.Info,
- warningLog: &Stats.Warning,
- errorLog: &Stats.Error,
+var severityStats = [severity.NumSeverity]*OutputStats{
+ severity.InfoLog: &Stats.Info,
+ severity.WarningLog: &Stats.Warning,
+ severity.ErrorLog: &Stats.Error,
}
// Level is exported because it appears in the arguments to V and is
@@ -408,7 +382,9 @@ type flushSyncWriter interface {
// init sets up the defaults and runs flushDaemon.
func init() {
- logging.stderrThreshold = errorLog // Default stderrThreshold is ERROR.
+ logging.stderrThreshold = severityValue{
+ Severity: severity.ErrorLog, // Default stderrThreshold is ERROR.
+ }
logging.setVState(0, nil, false)
logging.logDir = ""
logging.logFile = ""
@@ -459,20 +435,18 @@ type loggingT struct {
alsoToStderr bool // The -alsologtostderr flag.
// Level flag. Handled atomically.
- stderrThreshold severity // The -stderrthreshold flag.
+ stderrThreshold severityValue // The -stderrthreshold flag.
- // freeList is a list of byte buffers, maintained under freeListMu.
- freeList *buffer
- // freeListMu maintains the free list. It is separate from the main mutex
+ // bufferCache maintains the free list. It uses its own mutex
// so buffers can be grabbed and printed to without holding the main lock,
// for better parallelization.
- freeListMu sync.Mutex
+ bufferCache buffer.Buffers
// mu protects the remaining elements of this structure and is
// used to synchronize logging.
mu sync.Mutex
// file holds writer for each of the log types.
- file [numSeverity]flushSyncWriter
+ file [severity.NumSeverity]flushSyncWriter
// pcs is used in V to avoid an allocation when computing the caller's PC.
pcs [1]uintptr
// vmap is a cache of the V Level for each V() call site, identified by PC.
@@ -520,13 +494,6 @@ type loggingT struct {
filter LogFilter
}
-// buffer holds a byte Buffer for reuse. The zero value is ready for use.
-type buffer struct {
- bytes.Buffer
- tmp [64]byte // temporary byte array for creating headers.
- next *buffer
-}
-
var logging loggingT
// setVState sets a consistent state for V logging.
@@ -549,35 +516,6 @@ func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool
l.verbosity.set(verbosity)
}
-// getBuffer returns a new, ready-to-use buffer.
-func (l *loggingT) getBuffer() *buffer {
- l.freeListMu.Lock()
- b := l.freeList
- if b != nil {
- l.freeList = b.next
- }
- l.freeListMu.Unlock()
- if b == nil {
- b = new(buffer)
- } else {
- b.next = nil
- b.Reset()
- }
- return b
-}
-
-// putBuffer returns a buffer to the free list.
-func (l *loggingT) putBuffer(b *buffer) {
- if b.Len() >= 256 {
- // Let big buffers die a natural death.
- return
- }
- l.freeListMu.Lock()
- b.next = l.freeList
- l.freeList = b
- l.freeListMu.Unlock()
-}
-
var timeNow = time.Now // Stubbed out for testing.
/*
@@ -597,7 +535,7 @@ where the fields are defined as follows:
line The line number
msg The user-supplied message
*/
-func (l *loggingT) header(s severity, depth int) (*buffer, string, int) {
+func (l *loggingT) header(s severity.Severity, depth int) (*buffer.Buffer, string, int) {
_, file, line, ok := runtime.Caller(3 + depth)
if !ok {
file = "???"
@@ -617,100 +555,27 @@ func (l *loggingT) header(s severity, depth int) (*buffer, string, int) {
}
// formatHeader formats a log header using the provided file name and line number.
-func (l *loggingT) formatHeader(s severity, file string, line int) *buffer {
- now := timeNow()
- if line < 0 {
- line = 0 // not a real line number, but acceptable to someDigits
- }
- if s > fatalLog {
- s = infoLog // for safety.
- }
- buf := l.getBuffer()
+func (l *loggingT) formatHeader(s severity.Severity, file string, line int) *buffer.Buffer {
+ buf := l.bufferCache.GetBuffer()
if l.skipHeaders {
return buf
}
-
- // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand.
- // It's worth about 3X. Fprintf is hard.
- _, month, day := now.Date()
- hour, minute, second := now.Clock()
- // Lmmdd hh:mm:ss.uuuuuu threadid file:line]
- buf.tmp[0] = severityChar[s]
- buf.twoDigits(1, int(month))
- buf.twoDigits(3, day)
- buf.tmp[5] = ' '
- buf.twoDigits(6, hour)
- buf.tmp[8] = ':'
- buf.twoDigits(9, minute)
- buf.tmp[11] = ':'
- buf.twoDigits(12, second)
- buf.tmp[14] = '.'
- buf.nDigits(6, 15, now.Nanosecond()/1000, '0')
- buf.tmp[21] = ' '
- buf.nDigits(7, 22, pid, ' ') // TODO: should be TID
- buf.tmp[29] = ' '
- buf.Write(buf.tmp[:30])
- buf.WriteString(file)
- buf.tmp[0] = ':'
- n := buf.someDigits(1, line)
- buf.tmp[n+1] = ']'
- buf.tmp[n+2] = ' '
- buf.Write(buf.tmp[:n+3])
+ now := timeNow()
+ buf.FormatHeader(s, file, line, now)
return buf
}
-// Some custom tiny helper functions to print the log header efficiently.
-
-const digits = "0123456789"
-
-// twoDigits formats a zero-prefixed two-digit integer at buf.tmp[i].
-func (buf *buffer) twoDigits(i, d int) {
- buf.tmp[i+1] = digits[d%10]
- d /= 10
- buf.tmp[i] = digits[d%10]
-}
-
-// nDigits formats an n-digit integer at buf.tmp[i],
-// padding with pad on the left.
-// It assumes d >= 0.
-func (buf *buffer) nDigits(n, i, d int, pad byte) {
- j := n - 1
- for ; j >= 0 && d > 0; j-- {
- buf.tmp[i+j] = digits[d%10]
- d /= 10
- }
- for ; j >= 0; j-- {
- buf.tmp[i+j] = pad
- }
-}
-
-// someDigits formats a zero-prefixed variable-width integer at buf.tmp[i].
-func (buf *buffer) someDigits(i, d int) int {
- // Print into the top, then copy down. We know there's space for at least
- // a 10-digit number.
- j := len(buf.tmp)
- for {
- j--
- buf.tmp[j] = digits[d%10]
- d /= 10
- if d == 0 {
- break
- }
- }
- return copy(buf.tmp[i:], buf.tmp[j:])
-}
-
-func (l *loggingT) println(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
+func (l *loggingT) println(s severity.Severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
l.printlnDepth(s, logger, filter, 1, args...)
}
-func (l *loggingT) printlnDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
+func (l *loggingT) printlnDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
buf, file, line := l.header(s, depth)
// if logger is set, we clear the generated header as we rely on the backing
// logger implementation to print headers
if logger != nil {
- l.putBuffer(buf)
- buf = l.getBuffer()
+ l.bufferCache.PutBuffer(buf)
+ buf = l.bufferCache.GetBuffer()
}
if filter != nil {
args = filter.Filter(args)
@@ -719,17 +584,17 @@ func (l *loggingT) printlnDepth(s severity, logger *logr.Logger, filter LogFilte
l.output(s, logger, buf, depth, file, line, false)
}
-func (l *loggingT) print(s severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
+func (l *loggingT) print(s severity.Severity, logger *logr.Logger, filter LogFilter, args ...interface{}) {
l.printDepth(s, logger, filter, 1, args...)
}
-func (l *loggingT) printDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
+func (l *loggingT) printDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) {
buf, file, line := l.header(s, depth)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
if logger != nil {
- l.putBuffer(buf)
- buf = l.getBuffer()
+ l.bufferCache.PutBuffer(buf)
+ buf = l.bufferCache.GetBuffer()
}
if filter != nil {
args = filter.Filter(args)
@@ -741,17 +606,17 @@ func (l *loggingT) printDepth(s severity, logger *logr.Logger, filter LogFilter,
l.output(s, logger, buf, depth, file, line, false)
}
-func (l *loggingT) printf(s severity, logger *logr.Logger, filter LogFilter, format string, args ...interface{}) {
+func (l *loggingT) printf(s severity.Severity, logger *logr.Logger, filter LogFilter, format string, args ...interface{}) {
l.printfDepth(s, logger, filter, 1, format, args...)
}
-func (l *loggingT) printfDepth(s severity, logger *logr.Logger, filter LogFilter, depth int, format string, args ...interface{}) {
+func (l *loggingT) printfDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, format string, args ...interface{}) {
buf, file, line := l.header(s, depth)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
if logger != nil {
- l.putBuffer(buf)
- buf = l.getBuffer()
+ l.bufferCache.PutBuffer(buf)
+ buf = l.bufferCache.GetBuffer()
}
if filter != nil {
format, args = filter.FilterF(format, args)
@@ -766,13 +631,13 @@ func (l *loggingT) printfDepth(s severity, logger *logr.Logger, filter LogFilter
// printWithFileLine behaves like print but uses the provided file and line number. If
// alsoLogToStderr is true, the log message always appears on standard error; it
// will also appear in the log file unless --logtostderr is set.
-func (l *loggingT) printWithFileLine(s severity, logger *logr.Logger, filter LogFilter, file string, line int, alsoToStderr bool, args ...interface{}) {
+func (l *loggingT) printWithFileLine(s severity.Severity, logger *logr.Logger, filter LogFilter, file string, line int, alsoToStderr bool, args ...interface{}) {
buf := l.formatHeader(s, file, line)
// if logr is set, we clear the generated header as we rely on the backing
// logr implementation to print headers
if logger != nil {
- l.putBuffer(buf)
- buf = l.getBuffer()
+ l.bufferCache.PutBuffer(buf)
+ buf = l.bufferCache.GetBuffer()
}
if filter != nil {
args = filter.Filter(args)
@@ -793,7 +658,7 @@ func (l *loggingT) errorS(err error, logger *logr.Logger, filter LogFilter, dept
logger.WithCallDepth(depth+2).Error(err, msg, keysAndValues...)
return
}
- l.printS(err, errorLog, depth+1, msg, keysAndValues...)
+ l.printS(err, severity.ErrorLog, depth+1, msg, keysAndValues...)
}
// if loggr is specified, will call loggr.Info, otherwise output with logging module.
@@ -805,14 +670,14 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s
logger.WithCallDepth(depth+2).Info(msg, keysAndValues...)
return
}
- l.printS(nil, infoLog, depth+1, msg, keysAndValues...)
+ l.printS(nil, severity.InfoLog, depth+1, msg, keysAndValues...)
}
// printS is called from infoS and errorS if loggr is not specified.
// set log severity by s
-func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
+func (l *loggingT) printS(err error, s severity.Severity, depth int, msg string, keysAndValues ...interface{}) {
// Only create a new buffer if we don't have one cached.
- b := l.getBuffer()
+ b := l.bufferCache.GetBuffer()
// The message is always quoted, even if it contains line breaks.
// If developers want multi-line output, they should use a small, fixed
// message and put the multi-line output into a value.
@@ -823,7 +688,7 @@ func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndV
serialize.KVListFormat(&b.Buffer, keysAndValues...)
l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer)
// Make the buffer available for reuse.
- l.putBuffer(b)
+ l.bufferCache.PutBuffer(b)
}
// redirectBuffer is used to set an alternate destination for the logs
@@ -875,7 +740,7 @@ func ClearLogger() {
func SetOutput(w io.Writer) {
logging.mu.Lock()
defer logging.mu.Unlock()
- for s := fatalLog; s >= infoLog; s-- {
+ for s := severity.FatalLog; s >= severity.InfoLog; s-- {
rb := &redirectBuffer{
w: w,
}
@@ -887,7 +752,7 @@ func SetOutput(w io.Writer) {
func SetOutputBySeverity(name string, w io.Writer) {
logging.mu.Lock()
defer logging.mu.Unlock()
- sev, ok := severityByName(name)
+ sev, ok := severity.ByName(name)
if !ok {
panic(fmt.Sprintf("SetOutputBySeverity(%q): unrecognized severity name", name))
}
@@ -906,7 +771,7 @@ func LogToStderr(stderr bool) {
}
// output writes the data to the log files and releases the buffer.
-func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int, file string, line int, alsoToStderr bool) {
+func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buffer, depth int, file string, line int, alsoToStderr bool) {
var isLocked = true
l.mu.Lock()
defer func() {
@@ -925,8 +790,8 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
if log != nil {
// TODO: set 'severity' and caller information as structured log info
// keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line}
- if s == errorLog {
- l.logr.WithCallDepth(depth+3).Error(nil, string(data))
+ if s == severity.ErrorLog {
+ logging.logr.WithCallDepth(depth+3).Error(nil, string(data))
} else {
log.WithCallDepth(depth + 3).Info(string(data))
}
@@ -940,13 +805,13 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
if logging.logFile != "" {
// Since we are using a single log file, all of the items in l.file array
// will point to the same file, so just use one of them to write data.
- if l.file[infoLog] == nil {
- if err := l.createFiles(infoLog); err != nil {
+ if l.file[severity.InfoLog] == nil {
+ if err := l.createFiles(severity.InfoLog); err != nil {
os.Stderr.Write(data) // Make sure the message appears somewhere.
l.exit(err)
}
}
- l.file[infoLog].Write(data)
+ l.file[severity.InfoLog].Write(data)
} else {
if l.file[s] == nil {
if err := l.createFiles(s); err != nil {
@@ -959,22 +824,22 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
l.file[s].Write(data)
} else {
switch s {
- case fatalLog:
- l.file[fatalLog].Write(data)
+ case severity.FatalLog:
+ l.file[severity.FatalLog].Write(data)
fallthrough
- case errorLog:
- l.file[errorLog].Write(data)
+ case severity.ErrorLog:
+ l.file[severity.ErrorLog].Write(data)
fallthrough
- case warningLog:
- l.file[warningLog].Write(data)
+ case severity.WarningLog:
+ l.file[severity.WarningLog].Write(data)
fallthrough
- case infoLog:
- l.file[infoLog].Write(data)
+ case severity.InfoLog:
+ l.file[severity.InfoLog].Write(data)
}
}
}
}
- if s == fatalLog {
+ if s == severity.FatalLog {
// If we got here via Exit rather than Fatal, print no stacks.
if atomic.LoadUint32(&fatalNoStacks) > 0 {
l.mu.Unlock()
@@ -990,7 +855,7 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
}
// Write the stack trace for all goroutines to the files.
logExitFunc = func(error) {} // If we get a write error, we'll still exit below.
- for log := fatalLog; log >= infoLog; log-- {
+ for log := severity.FatalLog; log >= severity.InfoLog; log-- {
if f := l.file[log]; f != nil { // Can be nil if -logtostderr is set.
f.Write(trace)
}
@@ -1000,7 +865,7 @@ func (l *loggingT) output(s severity, log *logr.Logger, buf *buffer, depth int,
timeoutFlush(10 * time.Second)
os.Exit(255) // C++ uses -1, which is silly because it's anded(&) with 255 anyway.
}
- l.putBuffer(buf)
+ l.bufferCache.PutBuffer(buf)
if stats := severityStats[s]; stats != nil {
atomic.AddInt64(&stats.lines, 1)
@@ -1072,7 +937,7 @@ type syncBuffer struct {
logger *loggingT
*bufio.Writer
file *os.File
- sev severity
+ sev severity.Severity
nbytes uint64 // The number of bytes written to this file
maxbytes uint64 // The max number of bytes this syncBuffer.file can hold before cleaning up.
}
@@ -1118,7 +983,7 @@ func (sb *syncBuffer) rotateFile(now time.Time, startup bool) error {
sb.file.Close()
}
var err error
- sb.file, _, err = create(severityName[sb.sev], now, startup)
+ sb.file, _, err = create(severity.Name[sb.sev], now, startup)
if err != nil {
return err
}
@@ -1156,11 +1021,11 @@ const bufferSize = 256 * 1024
// createFiles creates all the log files for severity from sev down to infoLog.
// l.mu is held.
-func (l *loggingT) createFiles(sev severity) error {
+func (l *loggingT) createFiles(sev severity.Severity) error {
now := time.Now()
// Files are created in decreasing severity order, so as soon as we find one
// has already been created, we can stop.
- for s := sev; s >= infoLog && l.file[s] == nil; s-- {
+ for s := sev; s >= severity.InfoLog && l.file[s] == nil; s-- {
sb := &syncBuffer{
logger: l,
sev: s,
@@ -1194,7 +1059,7 @@ func (l *loggingT) lockAndFlushAll() {
// l.mu is held.
func (l *loggingT) flushAll() {
// Flush from fatal down, in case there's trouble flushing.
- for s := fatalLog; s >= infoLog; s-- {
+ for s := severity.FatalLog; s >= severity.InfoLog; s-- {
file := l.file[s]
if file != nil {
file.Flush() // ignore error
@@ -1211,7 +1076,7 @@ func (l *loggingT) flushAll() {
// Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not
// recognized, CopyStandardLogTo panics.
func CopyStandardLogTo(name string) {
- sev, ok := severityByName(name)
+ sev, ok := severity.ByName(name)
if !ok {
panic(fmt.Sprintf("log.CopyStandardLogTo(%q): unrecognized severity name", name))
}
@@ -1223,7 +1088,7 @@ func CopyStandardLogTo(name string) {
// logBridge provides the Write method that enables CopyStandardLogTo to connect
// Go's standard logs to the logs provided by this package.
-type logBridge severity
+type logBridge severity.Severity
// Write parses the standard logging line and passes its components to the
// logger for severity(lb).
@@ -1247,7 +1112,7 @@ func (lb logBridge) Write(b []byte) (n int, err error) {
}
// printWithFileLine with alsoToStderr=true, so standard log messages
// always appear on standard error.
- logging.printWithFileLine(severity(lb), logging.logr, logging.filter, file, line, true, text)
+ logging.printWithFileLine(severity.Severity(lb), logging.logr, logging.filter, file, line, true, text)
return len(b), nil
}
@@ -1352,7 +1217,7 @@ func (v Verbose) Enabled() bool {
// See the documentation of V for usage.
func (v Verbose) Info(args ...interface{}) {
if v.enabled {
- logging.print(infoLog, v.logr, logging.filter, args...)
+ logging.print(severity.InfoLog, v.logr, logging.filter, args...)
}
}
@@ -1360,7 +1225,7 @@ func (v Verbose) Info(args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfoDepth(depth int, args ...interface{}) {
if v.enabled {
- logging.printDepth(infoLog, v.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.InfoLog, v.logr, logging.filter, depth, args...)
}
}
@@ -1368,7 +1233,7 @@ func (v Verbose) InfoDepth(depth int, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) Infoln(args ...interface{}) {
if v.enabled {
- logging.println(infoLog, v.logr, logging.filter, args...)
+ logging.println(severity.InfoLog, v.logr, logging.filter, args...)
}
}
@@ -1376,7 +1241,7 @@ func (v Verbose) Infoln(args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfolnDepth(depth int, args ...interface{}) {
if v.enabled {
- logging.printlnDepth(infoLog, v.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.InfoLog, v.logr, logging.filter, depth, args...)
}
}
@@ -1384,7 +1249,7 @@ func (v Verbose) InfolnDepth(depth int, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) Infof(format string, args ...interface{}) {
if v.enabled {
- logging.printf(infoLog, v.logr, logging.filter, format, args...)
+ logging.printf(severity.InfoLog, v.logr, logging.filter, format, args...)
}
}
@@ -1392,7 +1257,7 @@ func (v Verbose) Infof(format string, args ...interface{}) {
// See the documentation of V for usage.
func (v Verbose) InfofDepth(depth int, format string, args ...interface{}) {
if v.enabled {
- logging.printfDepth(infoLog, v.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.InfoLog, v.logr, logging.filter, depth, format, args...)
}
}
@@ -1436,37 +1301,37 @@ func (v Verbose) ErrorS(err error, msg string, keysAndValues ...interface{}) {
// 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{}) {
- logging.print(infoLog, logging.logr, logging.filter, args...)
+ logging.print(severity.InfoLog, logging.logr, logging.filter, args...)
}
// InfoDepth acts as Info but uses depth to determine which call frame to log.
// InfoDepth(0, "msg") is the same as Info("msg").
func InfoDepth(depth int, args ...interface{}) {
- logging.printDepth(infoLog, logging.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.InfoLog, logging.logr, logging.filter, depth, args...)
}
// Infoln logs to the INFO log.
// Arguments are handled in the manner of fmt.Println; a newline is always appended.
func Infoln(args ...interface{}) {
- logging.println(infoLog, logging.logr, logging.filter, args...)
+ logging.println(severity.InfoLog, logging.logr, logging.filter, args...)
}
// InfolnDepth acts as Infoln but uses depth to determine which call frame to log.
// InfolnDepth(0, "msg") is the same as Infoln("msg").
func InfolnDepth(depth int, args ...interface{}) {
- logging.printlnDepth(infoLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.InfoLog, logging.logr, logging.filter, depth, args...)
}
// Infof logs to the INFO log.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Infof(format string, args ...interface{}) {
- logging.printf(infoLog, logging.logr, logging.filter, format, args...)
+ logging.printf(severity.InfoLog, logging.logr, logging.filter, format, args...)
}
// InfofDepth acts as Infof but uses depth to determine which call frame to log.
// InfofDepth(0, "msg", args...) is the same as Infof("msg", args...).
func InfofDepth(depth int, format string, args ...interface{}) {
- logging.printfDepth(infoLog, logging.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.InfoLog, logging.logr, logging.filter, depth, format, args...)
}
// InfoS structured logs to the INFO log.
@@ -1484,73 +1349,73 @@ func InfoS(msg string, keysAndValues ...interface{}) {
// 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{}) {
- logging.print(warningLog, logging.logr, logging.filter, args...)
+ logging.print(severity.WarningLog, logging.logr, logging.filter, args...)
}
// WarningDepth acts as Warning but uses depth to determine which call frame to log.
// WarningDepth(0, "msg") is the same as Warning("msg").
func WarningDepth(depth int, args ...interface{}) {
- logging.printDepth(warningLog, logging.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.WarningLog, logging.logr, logging.filter, depth, args...)
}
// Warningln logs to the WARNING and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is always appended.
func Warningln(args ...interface{}) {
- logging.println(warningLog, logging.logr, logging.filter, args...)
+ logging.println(severity.WarningLog, logging.logr, logging.filter, args...)
}
// WarninglnDepth acts as Warningln but uses depth to determine which call frame to log.
// WarninglnDepth(0, "msg") is the same as Warningln("msg").
func WarninglnDepth(depth int, args ...interface{}) {
- logging.printlnDepth(warningLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.WarningLog, logging.logr, logging.filter, depth, args...)
}
// Warningf logs to the WARNING and INFO logs.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Warningf(format string, args ...interface{}) {
- logging.printf(warningLog, logging.logr, logging.filter, format, args...)
+ logging.printf(severity.WarningLog, logging.logr, logging.filter, format, args...)
}
// WarningfDepth acts as Warningf but uses depth to determine which call frame to log.
// WarningfDepth(0, "msg", args...) is the same as Warningf("msg", args...).
func WarningfDepth(depth int, format string, args ...interface{}) {
- logging.printfDepth(warningLog, logging.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.WarningLog, logging.logr, logging.filter, depth, format, args...)
}
// Error logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
func Error(args ...interface{}) {
- logging.print(errorLog, logging.logr, logging.filter, args...)
+ logging.print(severity.ErrorLog, logging.logr, logging.filter, args...)
}
// ErrorDepth acts as Error but uses depth to determine which call frame to log.
// ErrorDepth(0, "msg") is the same as Error("msg").
func ErrorDepth(depth int, args ...interface{}) {
- logging.printDepth(errorLog, logging.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.ErrorLog, logging.logr, logging.filter, depth, args...)
}
// Errorln logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is always appended.
func Errorln(args ...interface{}) {
- logging.println(errorLog, logging.logr, logging.filter, args...)
+ logging.println(severity.ErrorLog, logging.logr, logging.filter, args...)
}
// ErrorlnDepth acts as Errorln but uses depth to determine which call frame to log.
// ErrorlnDepth(0, "msg") is the same as Errorln("msg").
func ErrorlnDepth(depth int, args ...interface{}) {
- logging.printlnDepth(errorLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.ErrorLog, logging.logr, logging.filter, depth, args...)
}
// Errorf logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Errorf(format string, args ...interface{}) {
- logging.printf(errorLog, logging.logr, logging.filter, format, args...)
+ logging.printf(severity.ErrorLog, logging.logr, logging.filter, format, args...)
}
// ErrorfDepth acts as Errorf but uses depth to determine which call frame to log.
// ErrorfDepth(0, "msg", args...) is the same as Errorf("msg", args...).
func ErrorfDepth(depth int, format string, args ...interface{}) {
- logging.printfDepth(errorLog, logging.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.ErrorLog, logging.logr, logging.filter, depth, format, args...)
}
// ErrorS structured logs to the ERROR, WARNING, and INFO logs.
@@ -1576,39 +1441,39 @@ func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{})
// 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.
func Fatal(args ...interface{}) {
- logging.print(fatalLog, logging.logr, logging.filter, args...)
+ logging.print(severity.FatalLog, logging.logr, logging.filter, args...)
}
// FatalDepth acts as Fatal but uses depth to determine which call frame to log.
// FatalDepth(0, "msg") is the same as Fatal("msg").
func FatalDepth(depth int, args ...interface{}) {
- logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.FatalLog, logging.logr, logging.filter, depth, args...)
}
// Fatalln 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.Println; a newline is always appended.
func Fatalln(args ...interface{}) {
- logging.println(fatalLog, logging.logr, logging.filter, args...)
+ logging.println(severity.FatalLog, logging.logr, logging.filter, args...)
}
// FatallnDepth acts as Fatalln but uses depth to determine which call frame to log.
// FatallnDepth(0, "msg") is the same as Fatalln("msg").
func FatallnDepth(depth int, args ...interface{}) {
- logging.printlnDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.FatalLog, logging.logr, logging.filter, depth, args...)
}
// Fatalf 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.Printf; a newline is appended if missing.
func Fatalf(format string, args ...interface{}) {
- logging.printf(fatalLog, logging.logr, logging.filter, format, args...)
+ logging.printf(severity.FatalLog, logging.logr, logging.filter, format, args...)
}
// FatalfDepth acts as Fatalf but uses depth to determine which call frame to log.
// FatalfDepth(0, "msg", args...) is the same as Fatalf("msg", args...).
func FatalfDepth(depth int, format string, args ...interface{}) {
- logging.printfDepth(fatalLog, logging.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.FatalLog, logging.logr, logging.filter, depth, format, args...)
}
// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks.
@@ -1619,41 +1484,41 @@ var fatalNoStacks uint32
// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
func Exit(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.print(fatalLog, logging.logr, logging.filter, args...)
+ logging.print(severity.FatalLog, logging.logr, logging.filter, args...)
}
// ExitDepth acts as Exit but uses depth to determine which call frame to log.
// ExitDepth(0, "msg") is the same as Exit("msg").
func ExitDepth(depth int, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.printDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printDepth(severity.FatalLog, logging.logr, logging.filter, depth, args...)
}
// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
func Exitln(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.println(fatalLog, logging.logr, logging.filter, args...)
+ logging.println(severity.FatalLog, logging.logr, logging.filter, args...)
}
// ExitlnDepth acts as Exitln but uses depth to determine which call frame to log.
// ExitlnDepth(0, "msg") is the same as Exitln("msg").
func ExitlnDepth(depth int, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.printlnDepth(fatalLog, logging.logr, logging.filter, depth, args...)
+ logging.printlnDepth(severity.FatalLog, logging.logr, logging.filter, depth, args...)
}
// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Exitf(format string, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.printf(fatalLog, logging.logr, logging.filter, format, args...)
+ logging.printf(severity.FatalLog, logging.logr, logging.filter, format, args...)
}
// ExitfDepth acts as Exitf but uses depth to determine which call frame to log.
// ExitfDepth(0, "msg", args...) is the same as Exitf("msg", args...).
func ExitfDepth(depth int, format string, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
- logging.printfDepth(fatalLog, logging.logr, logging.filter, depth, format, args...)
+ logging.printfDepth(severity.FatalLog, logging.logr, logging.filter, depth, format, args...)
}
// LogFilter is a collection of functions that can filter all logging calls,
diff --git a/klog_test.go b/klog_test.go
index f4bcc7764..6e1667639 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -36,6 +36,8 @@ import (
"github.com/go-logr/logr"
+ "k8s.io/klog/v2/internal/buffer"
+ "k8s.io/klog/v2/internal/severity"
"k8s.io/klog/v2/internal/test"
)
@@ -69,7 +71,7 @@ func (f *flushBuffer) Sync() error {
}
// swap sets the log writers and returns the old array.
-func (l *loggingT) swap(writers [numSeverity]flushSyncWriter) (old [numSeverity]flushSyncWriter) {
+func (l *loggingT) swap(writers [severity.NumSeverity]flushSyncWriter) (old [severity.NumSeverity]flushSyncWriter) {
l.mu.Lock()
defer l.mu.Unlock()
old = l.file
@@ -80,17 +82,17 @@ func (l *loggingT) swap(writers [numSeverity]flushSyncWriter) (old [numSeverity]
}
// newBuffers sets the log writers to all new byte buffers and returns the old array.
-func (l *loggingT) newBuffers() [numSeverity]flushSyncWriter {
- return l.swap([numSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
+func (l *loggingT) newBuffers() [severity.NumSeverity]flushSyncWriter {
+ return l.swap([severity.NumSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
}
// contents returns the specified log value as a string.
-func contents(s severity) string {
+func contents(s severity.Severity) string {
return logging.file[s].(*flushBuffer).String()
}
// contains reports whether the string is contained in the log.
-func contains(s severity, str string, t *testing.T) bool {
+func contains(s severity.Severity, str string, t *testing.T) bool {
return strings.Contains(contents(s), str)
}
@@ -105,10 +107,10 @@ func TestInfo(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Info("test")
- if !contains(infoLog, "I", t) {
- t.Errorf("Info has wrong character: %q", contents(infoLog))
+ if !contains(severity.InfoLog, "I", t) {
+ t.Errorf("Info has wrong character: %q", contents(severity.InfoLog))
}
- if !contains(infoLog, "test", t) {
+ if !contains(severity.InfoLog, "test", t) {
t.Error("Info failed")
}
}
@@ -124,7 +126,7 @@ func TestInfoDepth(t *testing.T) {
InfoDepth(0, "depth-test0")
f()
- msgs := strings.Split(strings.TrimSuffix(contents(infoLog), "\n"), "\n")
+ msgs := strings.Split(strings.TrimSuffix(contents(severity.InfoLog), "\n"), "\n")
if len(msgs) != 2 {
t.Fatalf("Got %d lines, expected 2", len(msgs))
}
@@ -176,10 +178,10 @@ func TestStandardLog(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
stdLog.Print("test")
- if !contains(infoLog, "I", t) {
- t.Errorf("Info has wrong character: %q", contents(infoLog))
+ if !contains(severity.InfoLog, "I", t) {
+ t.Errorf("Info has wrong character: %q", contents(severity.InfoLog))
}
- if !contains(infoLog, "test", t) {
+ if !contains(severity.InfoLog, "test", t) {
t.Error("Info failed")
}
}
@@ -192,19 +194,19 @@ func TestHeader(t *testing.T) {
timeNow = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
}
- pid = 1234
+ buffer.Pid = 1234
Info("test")
var line int
format := "I0102 15:04:05.067890 1234 klog_test.go:%d] test\n"
- n, err := fmt.Sscanf(contents(infoLog), format, &line)
+ n, err := fmt.Sscanf(contents(severity.InfoLog), format, &line)
if n != 1 || err != nil {
- t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
+ t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(severity.InfoLog))
}
// Scanf treats multiple spaces as equivalent to a single space,
// so check for correct space-padding also.
want := fmt.Sprintf(format, line)
- if contents(infoLog) != want {
- t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(infoLog), want)
+ if contents(severity.InfoLog) != want {
+ t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(severity.InfoLog), want)
}
}
@@ -219,8 +221,8 @@ func TestHeaderWithDir(t *testing.T) {
pid = 1234
Info("test")
re := regexp.MustCompile(`I0102 15:04:05.067890 1234 (klog|v2)/klog_test.go:(\d+)] test\n`)
- if !re.MatchString(contents(infoLog)) {
- t.Errorf("log format error: line does not match regex:\n\t%q\n", contents(infoLog))
+ if !re.MatchString(contents(severity.InfoLog)) {
+ t.Errorf("log format error: line does not match regex:\n\t%q\n", contents(severity.InfoLog))
}
}
@@ -231,17 +233,17 @@ func TestError(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Error("test")
- if !contains(errorLog, "E", t) {
- t.Errorf("Error has wrong character: %q", contents(errorLog))
+ if !contains(severity.ErrorLog, "E", t) {
+ t.Errorf("Error has wrong character: %q", contents(severity.ErrorLog))
}
- if !contains(errorLog, "test", t) {
+ if !contains(severity.ErrorLog, "test", t) {
t.Error("Error failed")
}
- str := contents(errorLog)
- if !contains(warningLog, str, t) {
+ str := contents(severity.ErrorLog)
+ if !contains(severity.WarningLog, str, t) {
t.Error("Warning failed")
}
- if !contains(infoLog, str, t) {
+ if !contains(severity.InfoLog, str, t) {
t.Error("Info failed")
}
}
@@ -258,17 +260,17 @@ func TestErrorWithOneOutput(t *testing.T) {
logging.oneOutput = false
}()
Error("test")
- if !contains(errorLog, "E", t) {
- t.Errorf("Error has wrong character: %q", contents(errorLog))
+ if !contains(severity.ErrorLog, "E", t) {
+ t.Errorf("Error has wrong character: %q", contents(severity.ErrorLog))
}
- if !contains(errorLog, "test", t) {
+ if !contains(severity.ErrorLog, "test", t) {
t.Error("Error failed")
}
- str := contents(errorLog)
- if contains(warningLog, str, t) {
+ str := contents(severity.ErrorLog)
+ if contains(severity.WarningLog, str, t) {
t.Error("Warning failed")
}
- if contains(infoLog, str, t) {
+ if contains(severity.InfoLog, str, t) {
t.Error("Info failed")
}
}
@@ -280,14 +282,14 @@ func TestWarning(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
Warning("test")
- if !contains(warningLog, "W", t) {
- t.Errorf("Warning has wrong character: %q", contents(warningLog))
+ if !contains(severity.WarningLog, "W", t) {
+ t.Errorf("Warning has wrong character: %q", contents(severity.WarningLog))
}
- if !contains(warningLog, "test", t) {
+ if !contains(severity.WarningLog, "test", t) {
t.Error("Warning failed")
}
- str := contents(warningLog)
- if !contains(infoLog, str, t) {
+ str := contents(severity.WarningLog)
+ if !contains(severity.InfoLog, str, t) {
t.Error("Info failed")
}
}
@@ -304,14 +306,14 @@ func TestWarningWithOneOutput(t *testing.T) {
logging.oneOutput = false
}()
Warning("test")
- if !contains(warningLog, "W", t) {
- t.Errorf("Warning has wrong character: %q", contents(warningLog))
+ if !contains(severity.WarningLog, "W", t) {
+ t.Errorf("Warning has wrong character: %q", contents(severity.WarningLog))
}
- if !contains(warningLog, "test", t) {
+ if !contains(severity.WarningLog, "test", t) {
t.Error("Warning failed")
}
- str := contents(warningLog)
- if contains(infoLog, str, t) {
+ str := contents(severity.WarningLog)
+ if contains(severity.InfoLog, str, t) {
t.Error("Info failed")
}
}
@@ -323,10 +325,10 @@ func TestV(t *testing.T) {
logging.verbosity.Set("2")
defer logging.verbosity.Set("0")
V(2).Info("test")
- if !contains(infoLog, "I", t) {
- t.Errorf("Info has wrong character: %q", contents(infoLog))
+ if !contains(severity.InfoLog, "I", t) {
+ t.Errorf("Info has wrong character: %q", contents(severity.InfoLog))
}
- if !contains(infoLog, "test", t) {
+ if !contains(severity.InfoLog, "test", t) {
t.Error("Info failed")
}
}
@@ -347,10 +349,10 @@ func TestVmoduleOn(t *testing.T) {
t.Error("V enabled for 3")
}
V(2).Info("test")
- if !contains(infoLog, "I", t) {
- t.Errorf("Info has wrong character: %q", contents(infoLog))
+ if !contains(severity.InfoLog, "I", t) {
+ t.Errorf("Info has wrong character: %q", contents(severity.InfoLog))
}
- if !contains(infoLog, "test", t) {
+ if !contains(severity.InfoLog, "test", t) {
t.Error("Info failed")
}
}
@@ -367,7 +369,7 @@ func TestVmoduleOff(t *testing.T) {
}
}
V(2).Info("test")
- if contents(infoLog) != "" {
+ if contents(severity.InfoLog) != "" {
t.Error("V logged incorrectly")
}
}
@@ -469,7 +471,7 @@ func TestRollover(t *testing.T) {
defer func(previous uint64) { MaxSize = previous }(MaxSize)
MaxSize = 512
Info("x") // Be sure we have a file.
- info, ok := logging.file[infoLog].(*syncBuffer)
+ info, ok := logging.file[severity.InfoLog].(*syncBuffer)
if !ok {
t.Fatal("info wasn't created")
}
@@ -530,7 +532,7 @@ func TestOpenAppendOnStart(t *testing.T) {
// Logging creates the file
Info(x)
- _, ok := logging.file[infoLog].(*syncBuffer)
+ _, ok := logging.file[severity.InfoLog].(*syncBuffer)
if !ok {
t.Fatal("info wasn't created")
}
@@ -595,7 +597,7 @@ func TestLogBacktraceAt(t *testing.T) {
setTraceLocation(file, line, ok, +2) // Two lines between Caller and Info calls.
Info("we want a stack trace here")
}
- numAppearances := strings.Count(contents(infoLog), infoLine)
+ numAppearances := strings.Count(contents(severity.InfoLog), infoLine)
if numAppearances < 2 {
// Need 2 appearances, one in the log header and one in the trace:
// log_test.go:281: I0511 16:36:06.952398 02238 log_test.go:280] we want a stack trace here
@@ -604,22 +606,22 @@ func TestLogBacktraceAt(t *testing.T) {
// ...
// We could be more precise but that would require knowing the details
// of the traceback format, which may not be dependable.
- t.Fatal("got no trace back; log is ", contents(infoLog))
+ t.Fatal("got no trace back; log is ", contents(severity.InfoLog))
}
}
func BenchmarkHeader(b *testing.B) {
for i := 0; i < b.N; i++ {
- buf, _, _ := logging.header(infoLog, 0)
- logging.putBuffer(buf)
+ buf, _, _ := logging.header(severity.InfoLog, 0)
+ logging.bufferCache.PutBuffer(buf)
}
}
func BenchmarkHeaderWithDir(b *testing.B) {
logging.addDirHeader = true
for i := 0; i < b.N; i++ {
- buf, _, _ := logging.header(infoLog, 0)
- logging.putBuffer(buf)
+ buf, _, _ := logging.header(severity.InfoLog, 0)
+ logging.bufferCache.PutBuffer(buf)
}
}
@@ -665,9 +667,11 @@ func BenchmarkLogs(b *testing.B) {
logging.verbosity.Set("0")
logging.toStderr = false
logging.alsoToStderr = false
- logging.stderrThreshold = fatalLog
+ logging.stderrThreshold = severityValue{
+ Severity: severity.FatalLog,
+ }
logging.logFile = testFile.Name()
- logging.swap([numSeverity]flushSyncWriter{nil, nil, nil, nil})
+ logging.swap([severity.NumSeverity]flushSyncWriter{nil, nil, nil, nil})
for i := 0; i < b.N; i++ {
Error("error")
@@ -775,8 +779,8 @@ func TestInfoObjectRef(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Info(tt.ref)
- if !contains(infoLog, tt.want, t) {
- t.Errorf("expected %v, got %v", tt.want, contents(infoLog))
+ if !contains(severity.InfoLog, tt.want, t) {
+ t.Errorf("expected %v, got %v", tt.want, contents(severity.InfoLog))
}
})
}
@@ -909,16 +913,16 @@ func TestInfoS(t *testing.T) {
}
for _, f := range functions {
for _, data := range testDataInfo {
- logging.file[infoLog] = &flushBuffer{}
+ logging.file[severity.InfoLog] = &flushBuffer{}
f(data.msg, data.keysValues...)
var line int
- n, err := fmt.Sscanf(contents(infoLog), data.format, &line)
+ n, err := fmt.Sscanf(contents(severity.InfoLog), data.format, &line)
if n != 1 || err != nil {
- t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
+ t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(severity.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)
+ if contents(severity.InfoLog) != want {
+ t.Errorf("InfoS has wrong format: \n got:\t%s\nwant:\t%s", contents(severity.InfoLog), want)
}
}
}
@@ -985,24 +989,24 @@ second value line`},
for l := Level(0); l < Level(4); l++ {
for _, data := range testDataInfo {
- logging.file[infoLog] = &flushBuffer{}
+ logging.file[severity.InfoLog] = &flushBuffer{}
V(l).InfoS(data.msg, data.keysValues...)
var want string
var line int
if l <= 2 {
- n, err := fmt.Sscanf(contents(infoLog), data.format, &line)
+ n, err := fmt.Sscanf(contents(severity.InfoLog), data.format, &line)
if n != 1 || err != nil {
- t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
+ t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(severity.InfoLog))
}
want = fmt.Sprintf(data.format, line)
} else {
want = ""
}
- if contents(infoLog) != want {
- t.Errorf("V(%d).InfoS has unexpected output:\ngot:\n%s\nwant:\n%s\n", l, contents(infoLog), want)
+ if contents(severity.InfoLog) != want {
+ t.Errorf("V(%d).InfoS has unexpected output:\ngot:\n%s\nwant:\n%s\n", l, contents(severity.InfoLog), want)
}
}
}
@@ -1037,16 +1041,16 @@ func TestErrorS(t *testing.T) {
},
}
for _, e := range errorList {
- logging.file[errorLog] = &flushBuffer{}
+ logging.file[severity.ErrorLog] = &flushBuffer{}
f(e.err, "Failed to update pod status", "pod", "kubedns")
var line int
- n, err := fmt.Sscanf(contents(errorLog), e.format, &line)
+ n, err := fmt.Sscanf(contents(severity.ErrorLog), e.format, &line)
if n != 1 || err != nil {
- t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(errorLog))
+ t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(severity.ErrorLog))
}
want := fmt.Sprintf(e.format, line)
- if contents(errorLog) != want {
- t.Errorf("ErrorS has wrong format:\ngot:\n%s\nwant:\n%s\n", contents(errorLog), want)
+ if contents(severity.ErrorLog) != want {
+ t.Errorf("ErrorS has wrong format:\ngot:\n%s\nwant:\n%s\n", contents(severity.ErrorLog), want)
}
}
}
@@ -1056,7 +1060,9 @@ func createTestValueOfLoggingT() *loggingT {
l := new(loggingT)
l.toStderr = true
l.alsoToStderr = false
- l.stderrThreshold = errorLog
+ l.stderrThreshold = severityValue{
+ Severity: severity.ErrorLog,
+ }
l.verbosity = Level(0)
l.skipHeaders = false
l.skipLogHeaders = false
@@ -1143,116 +1149,116 @@ func TestLogFilter(t *testing.T) {
funcs := []struct {
name string
logFunc func(args ...interface{})
- severity severity
+ severity severity.Severity
}{{
name: "Info",
logFunc: Info,
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "InfoDepth",
logFunc: func(args ...interface{}) {
InfoDepth(1, args...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "Infoln",
logFunc: Infoln,
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "Infof",
logFunc: func(args ...interface{}) {
Infof(args[0].(string), args[1:]...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "InfoS",
logFunc: func(args ...interface{}) {
InfoS(args[0].(string), args[1:]...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "Warning",
logFunc: Warning,
- severity: warningLog,
+ severity: severity.WarningLog,
}, {
name: "WarningDepth",
logFunc: func(args ...interface{}) {
WarningDepth(1, args...)
},
- severity: warningLog,
+ severity: severity.WarningLog,
}, {
name: "Warningln",
logFunc: Warningln,
- severity: warningLog,
+ severity: severity.WarningLog,
}, {
name: "Warningf",
logFunc: func(args ...interface{}) {
Warningf(args[0].(string), args[1:]...)
},
- severity: warningLog,
+ severity: severity.WarningLog,
}, {
name: "Error",
logFunc: Error,
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "ErrorDepth",
logFunc: func(args ...interface{}) {
ErrorDepth(1, args...)
},
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "Errorln",
logFunc: Errorln,
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "Errorf",
logFunc: func(args ...interface{}) {
Errorf(args[0].(string), args[1:]...)
},
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "ErrorS",
logFunc: func(args ...interface{}) {
ErrorS(errors.New("testerror"), args[0].(string), args[1:]...)
},
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "V().Info",
logFunc: func(args ...interface{}) {
V(0).Info(args...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "V().Infoln",
logFunc: func(args ...interface{}) {
V(0).Infoln(args...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "V().Infof",
logFunc: func(args ...interface{}) {
V(0).Infof(args[0].(string), args[1:]...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "V().InfoS",
logFunc: func(args ...interface{}) {
V(0).InfoS(args[0].(string), args[1:]...)
},
- severity: infoLog,
+ severity: severity.InfoLog,
}, {
name: "V().Error",
logFunc: func(args ...interface{}) {
V(0).Error(errors.New("test error"), args[0].(string), args[1:]...)
},
- severity: errorLog,
+ severity: severity.ErrorLog,
}, {
name: "V().ErrorS",
logFunc: func(args ...interface{}) {
V(0).ErrorS(errors.New("test error"), args[0].(string), args[1:]...)
},
- severity: errorLog,
+ severity: severity.ErrorLog,
}}
testcases := []struct {
@@ -1291,13 +1297,13 @@ func TestInfoWithLogr(t *testing.T) {
}{{
msg: "foo",
expected: testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: "foo\n",
},
}, {
msg: "",
expected: testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: "\n",
},
}}
@@ -1329,7 +1335,7 @@ func TestInfoSWithLogr(t *testing.T) {
msg: "foo",
keysValues: []interface{}{},
expected: testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: "foo",
keysAndValues: []interface{}{},
},
@@ -1337,7 +1343,7 @@ func TestInfoSWithLogr(t *testing.T) {
msg: "bar",
keysValues: []interface{}{"a", 1},
expected: testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: "bar",
keysAndValues: []interface{}{"a", 1},
},
@@ -1374,7 +1380,7 @@ func TestErrorSWithLogr(t *testing.T) {
msg: "foo1",
keysValues: []interface{}{},
expected: testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: "foo1",
keysAndValues: []interface{}{},
err: testError,
@@ -1384,7 +1390,7 @@ func TestErrorSWithLogr(t *testing.T) {
msg: "bar1",
keysValues: []interface{}{"a", 1},
expected: testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: "bar1",
keysAndValues: []interface{}{"a", 1},
err: testError,
@@ -1394,7 +1400,7 @@ func TestErrorSWithLogr(t *testing.T) {
msg: "foo2",
keysValues: []interface{}{},
expected: testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: "foo2",
keysAndValues: []interface{}{},
err: nil,
@@ -1404,7 +1410,7 @@ func TestErrorSWithLogr(t *testing.T) {
msg: "bar2",
keysValues: []interface{}{"a", 1},
expected: testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: "bar2",
keysAndValues: []interface{}{"a", 1},
err: nil,
@@ -1598,7 +1604,7 @@ type testLogr struct {
}
type testLogrEntry struct {
- severity severity
+ severity severity.Severity
msg string
keysAndValues []interface{}
err error
@@ -1614,7 +1620,7 @@ func (l *testLogr) Info(level int, msg string, keysAndValues ...interface{}) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.entries = append(l.entries, testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: msg,
keysAndValues: keysAndValues,
})
@@ -1624,7 +1630,7 @@ func (l *testLogr) Error(err error, msg string, keysAndValues ...interface{}) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.entries = append(l.entries, testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: msg,
keysAndValues: keysAndValues,
err: err,
@@ -1669,7 +1675,7 @@ func (l *callDepthTestLogr) Info(level int, msg string, keysAndValues ...interfa
// test case.
_, file, line, _ := runtime.Caller(l.callDepth + 2)
l.entries = append(l.entries, testLogrEntry{
- severity: infoLog,
+ severity: severity.InfoLog,
msg: msg,
keysAndValues: append([]interface{}{file, line}, keysAndValues...),
})
@@ -1682,7 +1688,7 @@ func (l *callDepthTestLogr) Error(err error, msg string, keysAndValues ...interf
// test case.
_, file, line, _ := runtime.Caller(l.callDepth + 2)
l.entries = append(l.entries, testLogrEntry{
- severity: errorLog,
+ severity: severity.ErrorLog,
msg: msg,
keysAndValues: append([]interface{}{file, line}, keysAndValues...),
err: err,
From cda3be772d092b380a509229d775a5a1d337f436 Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 16:53:28 +0100
Subject: [PATCH 038/125] klogr: report missing values with "(MISSING)"
klogr had a special value for a missing value in a key/value pair, so the
intention probably was to use that as replacement, the same way as klog does
it. But because of a bug in the code which removed duplicates, it treated a
missing value like nil. Now the behavior is consistent with klog.
---
internal/serialize/keyvalues.go | 4 ++++
klogr/klogr_test.go | 8 ++++----
test/output_test.go | 10 ++--------
3 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go
index 479a37351..c3211ec58 100644
--- a/internal/serialize/keyvalues.go
+++ b/internal/serialize/keyvalues.go
@@ -36,7 +36,11 @@ func TrimDuplicates(kvLists ...[]interface{}) [][]interface{} {
// initialise this output slice
outs[i] = []interface{}{}
// obtain a reference to the kvList we are processing
+ // and make sure it has an even number of entries
kvList := kvLists[i]
+ if len(kvList)%2 != 0 {
+ kvList = append(kvList, missingValue)
+ }
// start iterating at len(kvList) - 2 (i.e. the 2nd last item) for
// slices that have an even number of elements.
diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go
index 7775b436e..45565b271 100644
--- a/klogr/klogr_test.go
+++ b/klogr/klogr_test.go
@@ -113,9 +113,9 @@ func testOutput(t *testing.T, format string) {
klogr: new(),
text: "test",
keysAndValues: []interface{}{"akey", "avalue", "akey2"},
- expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"=null
+ expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)"
`,
- expectedKlogOutput: `"test" akey="avalue" akey2=
+ expectedKlogOutput: `"test" akey="avalue" akey2="(MISSING)"
`,
},
"should correctly html characters": {
@@ -131,9 +131,9 @@ func testOutput(t *testing.T, format string) {
klogr: new().WithValues("basekey1", "basevar1", "basekey2"),
text: "test",
keysAndValues: []interface{}{"akey", "avalue", "akey2"},
- expectedOutput: ` "msg"="test" "basekey1"="basevar1" "basekey2"=null "akey"="avalue" "akey2"=null
+ expectedOutput: ` "msg"="test" "basekey1"="basevar1" "basekey2"="(MISSING)" "akey"="avalue" "akey2"="(MISSING)"
`,
- expectedKlogOutput: `"test" basekey1="basevar1" basekey2= akey="avalue" akey2=
+ expectedKlogOutput: `"test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
`,
},
"should correctly print regular error types": {
diff --git a/test/output_test.go b/test/output_test.go
index 9b3161761..ddb47044b 100644
--- a/test/output_test.go
+++ b/test/output_test.go
@@ -42,15 +42,9 @@ func TestKlogrOutput(t *testing.T) {
`I output.go:] "test" keyWithoutValue="(MISSING)"
I output.go:] "test" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
I output.go:] "test" keyWithoutValue="(MISSING)"
-`: `I output.go:] "test" keyWithoutValue=
+`: `I output.go:] "test" keyWithoutValue="(MISSING)"
I output.go:] "test" keyWithoutValue="anotherKeyWithoutValue"
-I output.go:] "test" keyWithoutValue=
-`,
- `I output.go:] "test" akey="avalue" akey2="(MISSING)"
-`: `I output.go:] "test" akey="avalue" akey2=
-`,
- `I output.go:] "test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
-`: `I output.go:] "test" basekey1="basevar1" basekey2= akey="avalue" akey2=
+I output.go:] "test" keyWithoutValue="(MISSING)"
`,
}
Output(t, OutputConfig{
From 5938b0a28c269b21250bc8ae5ff6740f29286f7c Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 20:01:11 +0100
Subject: [PATCH 039/125] klogr: improve handling of missing value in
WithValues
This is invalid:
logger.WithValue("keyWithoutValue").WithValue("anotherKeyWithoutValue")
Because it concatenated key/values without checking, klogr treated this like:
logger.WithValue("keyWithoutValue", "anotherKeyWithoutValue")
Now the parameters are checked and a "(MISSING)" is appended as in the other
cases where a missing value is detected.
---
internal/serialize/keyvalues.go | 22 ++++++++++++++++++++++
klogr/klogr.go | 4 +---
klogr/klogr_test.go | 9 +++++++++
test/output_test.go | 12 ------------
4 files changed, 32 insertions(+), 15 deletions(-)
diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go
index c3211ec58..4f583912f 100644
--- a/internal/serialize/keyvalues.go
+++ b/internal/serialize/keyvalues.go
@@ -22,6 +22,28 @@ import (
"strconv"
)
+// WithValues implements LogSink.WithValues. The old key/value pairs are
+// assumed to be well-formed, the new ones are checked and padded if
+// necessary. It returns a new slice.
+func WithValues(oldKV, newKV []interface{}) []interface{} {
+ if len(newKV) == 0 {
+ return oldKV
+ }
+ newLen := len(oldKV) + len(newKV)
+ hasMissingValue := newLen%2 != 0
+ if hasMissingValue {
+ newLen++
+ }
+ // The new LogSink must have its own slice.
+ kv := make([]interface{}, 0, newLen)
+ kv = append(kv, oldKV...)
+ kv = append(kv, newKV...)
+ if hasMissingValue {
+ kv = append(kv, missingValue)
+ }
+ return kv
+}
+
// TrimDuplicates deduplicates elements provided in multiple key/value tuple
// slices, whilst maintaining the distinction between where the items are
// contained.
diff --git a/klogr/klogr.go b/klogr/klogr.go
index d8e660e45..02433e844 100644
--- a/klogr/klogr.go
+++ b/klogr/klogr.go
@@ -170,9 +170,7 @@ func (l klogger) WithName(name string) logr.LogSink {
}
func (l klogger) WithValues(kvList ...interface{}) logr.LogSink {
- // Three slice args forces a copy.
- n := len(l.values)
- l.values = append(l.values[:n:n], kvList...)
+ l.values = serialize.WithValues(l.values, kvList)
return &l
}
diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go
index 45565b271..ab3a525a5 100644
--- a/klogr/klogr_test.go
+++ b/klogr/klogr_test.go
@@ -116,6 +116,15 @@ func testOutput(t *testing.T, format string) {
expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)"
`,
expectedKlogOutput: `"test" akey="avalue" akey2="(MISSING)"
+`,
+ },
+ "should correctly handle odd-numbers of KVs in WithValue": {
+ klogr: new().WithValues("keyWithoutValue"),
+ text: "test",
+ keysAndValues: []interface{}{"akey", "avalue", "akey2"},
+ expectedOutput: ` "msg"="test" "keyWithoutValue"="(MISSING)" "akey"="avalue" "akey2"="(MISSING)"
+`,
+ expectedKlogOutput: `"test" keyWithoutValue="(MISSING)" akey="avalue" akey2="(MISSING)"
`,
},
"should correctly html characters": {
diff --git a/test/output_test.go b/test/output_test.go
index ddb47044b..8dad50b6e 100644
--- a/test/output_test.go
+++ b/test/output_test.go
@@ -36,21 +36,9 @@ func TestKlogOutput(t *testing.T) {
// TestKlogrOutput tests klogr output via klog.
func TestKlogrOutput(t *testing.T) {
- // klogr currently doesn't produce exactly the same output as klog.
- // TODO: fix that.
- mapping := map[string]string{
- `I output.go:] "test" keyWithoutValue="(MISSING)"
-I output.go:] "test" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
-I output.go:] "test" keyWithoutValue="(MISSING)"
-`: `I output.go:] "test" keyWithoutValue="(MISSING)"
-I output.go:] "test" keyWithoutValue="anotherKeyWithoutValue"
-I output.go:] "test" keyWithoutValue="(MISSING)"
-`,
- }
Output(t, OutputConfig{
NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger {
return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
},
- ExpectedOutputMapping: mapping,
})
}
From 71ac98f1c83165bf1b83b8be5f2a5911f077a18a Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Wed, 9 Feb 2022 11:58:09 +0100
Subject: [PATCH 040/125] textlogger: add logger which emulates the traditional
output
In contrast to klogr, the formatting and printing is handled entirely
by this logger without depending on klog.go.
The intention is to use this in two ways:
- for testing of SetLogger
- as simpler text logger in Kubernetes for contextual logging
---
test/output_test.go | 21 +++++
textlogger/options.go | 139 +++++++++++++++++++++++++++++++++
textlogger/textlogger.go | 163 +++++++++++++++++++++++++++++++++++++++
3 files changed, 323 insertions(+)
create mode 100644 textlogger/options.go
create mode 100644 textlogger/textlogger.go
diff --git a/test/output_test.go b/test/output_test.go
index 8dad50b6e..5a43a1ced 100644
--- a/test/output_test.go
+++ b/test/output_test.go
@@ -23,6 +23,7 @@ import (
"github.com/go-logr/logr"
"k8s.io/klog/v2/klogr"
+ "k8s.io/klog/v2/textlogger"
)
func init() {
@@ -34,6 +35,26 @@ func TestKlogOutput(t *testing.T) {
Output(t, OutputConfig{})
}
+// TestTextloggerOutput tests the textlogger, directly and as backend.
+func TestTextloggerOutput(t *testing.T) {
+ newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
+ config := textlogger.NewConfig(
+ textlogger.Verbosity(v),
+ textlogger.Output(out),
+ )
+ if err := config.VModule().Set(vmodule); err != nil {
+ panic(err)
+ }
+ return textlogger.NewLogger(config)
+ }
+ t.Run("direct", func(t *testing.T) {
+ Output(t, OutputConfig{NewLogger: newLogger, SupportsVModule: true})
+ })
+ t.Run("klog-backend", func(t *testing.T) {
+ Output(t, OutputConfig{NewLogger: newLogger, AsBackend: true})
+ })
+}
+
// TestKlogrOutput tests klogr output via klog.
func TestKlogrOutput(t *testing.T) {
Output(t, OutputConfig{
diff --git a/textlogger/options.go b/textlogger/options.go
new file mode 100644
index 000000000..28188ddf3
--- /dev/null
+++ b/textlogger/options.go
@@ -0,0 +1,139 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package textlogger
+
+import (
+ "flag"
+ "io"
+ "os"
+ "strconv"
+
+ "k8s.io/klog/v2/internal/verbosity"
+)
+
+// Config influences logging in a text logger. To make this configurable via
+// command line flags, instantiate this once per program and use AddFlags to
+// bind command line flags to the instance before passing it to NewTestContext.
+//
+// Must be constructed with NewConfig.
+//
+// Experimental
+//
+// Notice: This type is EXPERIMENTAL and may be changed or removed in a
+// later release.
+type Config struct {
+ *verbosity.VState
+ co configOptions
+}
+
+// ConfigOption implements functional parameters for NewConfig.
+//
+// Experimental
+//
+// Notice: This type is EXPERIMENTAL and may be changed or removed in a
+// later release.
+type ConfigOption func(co *configOptions)
+
+type configOptions struct {
+ verbosityFlagName string
+ vmoduleFlagName string
+ verbosityDefault int
+ output io.Writer
+}
+
+// VerbosityFlagName overrides the default -v for the verbosity level.
+func VerbosityFlagName(name string) ConfigOption {
+ return func(co *configOptions) {
+
+ co.verbosityFlagName = name
+ }
+}
+
+// VModulFlagName overrides the default -vmodule for the per-module
+// verbosity levels.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func VModuleFlagName(name string) ConfigOption {
+ return func(co *configOptions) {
+ co.vmoduleFlagName = name
+ }
+}
+
+// Verbosity overrides the default verbosity level of 0.
+// See https://github.com/kubernetes/community/blob/9406b4352fe2d5810cb21cc3cb059ce5886de157/contributors/devel/sig-instrumentation/logging.md#logging-conventions
+// for log level conventions in Kubernetes.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func Verbosity(level int) ConfigOption {
+ return func(co *configOptions) {
+ co.verbosityDefault = level
+ }
+}
+
+// Output overrides stderr as the output stream.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func Output(output io.Writer) ConfigOption {
+ return func(co *configOptions) {
+ co.output = output
+ }
+}
+
+// NewConfig returns a configuration with recommended defaults and optional
+// modifications. Command line flags are not bound to any FlagSet yet.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func NewConfig(opts ...ConfigOption) *Config {
+ c := &Config{
+ VState: verbosity.New(),
+ co: configOptions{
+ verbosityFlagName: "v",
+ vmoduleFlagName: "vmodule",
+ verbosityDefault: 0,
+ output: os.Stderr,
+ },
+ }
+ for _, opt := range opts {
+ opt(&c.co)
+ }
+
+ c.V().Set(strconv.FormatInt(int64(c.co.verbosityDefault), 10))
+ return c
+}
+
+// AddFlags registers the command line flags that control the configuration.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func (c *Config) AddFlags(fs *flag.FlagSet) {
+ fs.Var(c.V(), c.co.verbosityFlagName, "number for the log level verbosity of the testing logger")
+ fs.Var(c.VModule(), c.co.vmoduleFlagName, "comma-separated list of pattern=N log level settings for files matching the patterns")
+}
diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go
new file mode 100644
index 000000000..2c0ec88ff
--- /dev/null
+++ b/textlogger/textlogger.go
@@ -0,0 +1,163 @@
+/*
+Copyright 2019 The Kubernetes Authors.
+Copyright 2020 Intel Coporation.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package textlogger contains an implementation of the logr interface
+// which is producing the exact same output as klog.
+//
+// Experimental
+//
+// Notice: This package is EXPERIMENTAL and may be changed or removed in a
+// later release.
+package textlogger
+
+import (
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-logr/logr"
+
+ "k8s.io/klog/v2/internal/buffer"
+ "k8s.io/klog/v2/internal/serialize"
+ "k8s.io/klog/v2/internal/severity"
+ "k8s.io/klog/v2/internal/verbosity"
+)
+
+var (
+ // TimeNow is used to retrieve the current time. May be changed for testing.
+ //
+ // Experimental
+ //
+ // Notice: This variable is EXPERIMENTAL and may be changed or removed in a
+ // later release.
+ TimeNow = time.Now
+)
+
+// NewLogger constructs a new logger.
+//
+// Experimental
+//
+// Notice: This function is EXPERIMENTAL and may be changed or removed in a
+// later release. The behavior of the returned Logger may change.
+func NewLogger(c *Config) logr.Logger {
+ return logr.New(&tlogger{
+ prefix: "",
+ values: nil,
+ config: c,
+ bufferCache: &buffer.Buffers{},
+ })
+}
+
+type tlogger struct {
+ callDepth int
+ prefix string
+ values []interface{}
+ config *Config
+ bufferCache *buffer.Buffers
+}
+
+func copySlice(in []interface{}) []interface{} {
+ out := make([]interface{}, len(in))
+ copy(out, in)
+ return out
+}
+
+func (l *tlogger) Init(info logr.RuntimeInfo) {
+ l.callDepth = info.CallDepth
+}
+
+func (l *tlogger) WithCallDepth(depth int) logr.LogSink {
+ newLogger := *l
+ newLogger.callDepth += depth
+ return &newLogger
+}
+
+func (l *tlogger) Enabled(level int) bool {
+ return l.config.Enabled(verbosity.Level(level), 1)
+}
+
+func (l *tlogger) Info(level int, msg string, kvList ...interface{}) {
+ l.print(nil, severity.InfoLog, msg, kvList)
+}
+
+func (l *tlogger) Error(err error, msg string, kvList ...interface{}) {
+ l.print(err, severity.ErrorLog, msg, kvList)
+}
+
+func (l *tlogger) print(err error, s severity.Severity, msg string, kvList []interface{}) {
+ // Only create a new buffer if we don't have one cached.
+ b := l.bufferCache.GetBuffer()
+
+ // Determine caller.
+ // +1 for this frame, +1 for Info/Error.
+ _, file, line, ok := runtime.Caller(l.callDepth + 2)
+ if !ok {
+ file = "???"
+ line = 1
+ } else {
+ if slash := strings.LastIndex(file, "/"); slash >= 0 {
+ path := file
+ file = path[slash+1:]
+ }
+ }
+
+ // Format header.
+ now := TimeNow()
+ b.FormatHeader(s, file, line, now)
+
+ // Inject WithName names into message.
+ if l.prefix != "" {
+ msg = l.prefix + ": " + msg
+ }
+
+ // The message is always quoted, even if it contains line breaks.
+ // If developers want multi-line output, they should use a small, fixed
+ // message and put the multi-line output into a value.
+ b.WriteString(strconv.Quote(msg))
+ if err != nil {
+ serialize.KVListFormat(&b.Buffer, "err", err)
+ }
+ trimmed := serialize.TrimDuplicates(l.values, kvList)
+ serialize.KVListFormat(&b.Buffer, trimmed[0]...)
+ serialize.KVListFormat(&b.Buffer, trimmed[1]...)
+ if b.Len() == 0 || b.Bytes()[b.Len()-1] != '\n' {
+ b.WriteByte('\n')
+ }
+ l.config.co.output.Write(b.Bytes())
+}
+
+// WithName returns a new logr.Logger with the specified name appended. klogr
+// uses '/' characters to separate name elements. Callers should not pass '/'
+// in the provided name string, but this library does not actually enforce that.
+func (l *tlogger) WithName(name string) logr.LogSink {
+ new := *l
+ if len(l.prefix) > 0 {
+ new.prefix = l.prefix + "/"
+ }
+ new.prefix += name
+ return &new
+}
+
+func (l *tlogger) WithValues(kvList ...interface{}) logr.LogSink {
+ new := *l
+ new.values = serialize.WithValues(l.values, kvList)
+ return &new
+}
+
+var _ logr.LogSink = &tlogger{}
+var _ logr.CallDepthLogSink = &tlogger{}
From bb978b3d0aef19ebcc75f2a166f2f123f388a76d Mon Sep 17 00:00:00 2001
From: Patrick Ohly
Date: Mon, 14 Feb 2022 19:12:50 +0100
Subject: [PATCH 041/125] test: move output test to example, include zapr
To reproduce the loss of verbosity in
klogr (https://github.com/kubernetes/klog/issues/294) we need a backend which
actually emits the verbosity. zapr does that and also happens to be the logging
implementation used in Kubernetes for JSON, so it makes sense to test with
that.
The expected output mapping may be useful also for testing in Kubernetes,
therefore it gets exported by k8s.io/klog/v2/test.
Because k8s.io/klog/v2 must not depend on zapr, the actual output test gets
moved to the examples. The downside is that "go test ./..." no longer runs it.
One has to remember to enter the "examples" directory. The GitHub action gets
updated to do that.
---
.github/workflows/test.yml | 3 +-
examples/go.mod | 3 +
examples/go.sum | 58 ++++++-
examples/output_test/output_test.go | 158 ++++++++++++++++++
test/output.go | 49 ++++--
test/output_test.go | 65 --------
test/zapr.go | 246 ++++++++++++++++++++++++++++
7 files changed, 493 insertions(+), 89 deletions(-)
create mode 100644 examples/output_test/output_test.go
delete mode 100644 test/output_test.go
create mode 100644 test/zapr.go
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1e6806d1c..cebf8602f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,7 +18,8 @@ jobs:
run: |
go get -t -v ./...
go test -v -race ./...
- cd hack/tools && go test -v -race ./...
+ cd hack/tools && go test -v -race ./... && cd ../..
+ cd examples && go test -v -race ./... && cd ..
lint:
runs-on: ubuntu-latest
steps:
diff --git a/examples/go.mod b/examples/go.mod
index 2768f2c1e..d7956666e 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -3,7 +3,10 @@ module example
go 1.13
require (
+ github.com/go-logr/logr v1.2.2
+ github.com/go-logr/zapr v1.2.2
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+ go.uber.org/zap v1.19.0
k8s.io/klog/v2 v2.30.0
)
diff --git a/examples/go.sum b/examples/go.sum
index 4d41cc903..5b43f393e 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,8 +1,56 @@
-github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
-github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
-github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/zapr v1.2.2 h1:5YNlIL6oZLydaV4dOFjL8YpgXF/tPeTbnpatnu3cq6o=
+github.com/go-logr/zapr v1.2.2/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc h1:E/enZ+SqXD3ChluFNvXqlLcUkqMQQDpiyGruRq5pjvY=
-k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc/go.mod h1:q4PVo0BneA7GsUJvFqoEvOCVmYJP0c5Y4VxrAYpJrIk=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
+go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go
new file mode 100644
index 000000000..a6ef0abb2
--- /dev/null
+++ b/examples/output_test/output_test.go
@@ -0,0 +1,158 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package output_test shows how to use k8s.io/klog/v2/test
+// and provides unit testing with dependencies that wouldn't
+// be acceptable for the main module.
+package output_test
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/go-logr/zapr"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+
+ "k8s.io/klog/v2"
+ "k8s.io/klog/v2/klogr"
+ "k8s.io/klog/v2/test"
+ "k8s.io/klog/v2/textlogger"
+)
+
+func init() {
+ test.InitKlog()
+}
+
+// TestKlogOutput tests klog output without a logger.
+func TestKlogOutput(t *testing.T) {
+ test.Output(t, test.OutputConfig{})
+}
+
+// TestTextloggerOutput tests the textlogger, directly and as backend.
+func TestTextloggerOutput(t *testing.T) {
+ newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
+ config := textlogger.NewConfig(
+ textlogger.Verbosity(v),
+ textlogger.Output(out),
+ )
+ if err := config.VModule().Set(vmodule); err != nil {
+ panic(err)
+ }
+ return textlogger.NewLogger(config)
+ }
+ t.Run("direct", func(t *testing.T) {
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true})
+ })
+ t.Run("klog-backend", func(t *testing.T) {
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, AsBackend: true})
+ })
+}
+
+// TestTextloggerOutput tests the zapr, directly and as backend.
+func TestZaprOutput(t *testing.T) {
+ newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
+ return newZaprLogger(out, v)
+ }
+ t.Run("direct", func(t *testing.T) {
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: test.ZaprOutputMappingDirect()})
+ })
+ t.Run("klog-backend", func(t *testing.T) {
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, AsBackend: true, ExpectedOutputMapping: test.ZaprOutputMappingIndirect()})
+ })
+}
+
+// TestKlogrOutput tests klogr output via klog.
+func TestKlogrOutput(t *testing.T) {
+ test.Output(t, test.OutputConfig{
+ NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger {
+ return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
+ },
+ })
+}
+
+// TestKlogrStackText tests klogr -> klog -> text logger.
+func TestKlogrStackText(t *testing.T) {
+ newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
+ // Backend: text output.
+ config := textlogger.NewConfig(
+ textlogger.Verbosity(v),
+ textlogger.Output(out),
+ )
+ if err := config.VModule().Set(vmodule); err != nil {
+ panic(err)
+ }
+ klog.SetLogger(textlogger.NewLogger(config))
+
+ // Frontend: klogr.
+ return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
+ }
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true})
+}
+
+// TestKlogrStackKlogr tests klogr -> klog -> zapr.
+//
+// This exposes whether verbosity is passed through correctly
+// (https://github.com/kubernetes/klog/issues/294) because klogr logging
+// records that.
+func TestKlogrStackZapr(t *testing.T) {
+ mapping := test.ZaprOutputMappingIndirect()
+ for key, value := range mapping {
+ // BUG (https://github.com/kubernetes/klog/issues/294): verbosity is lost
+ mapping[key] = strings.ReplaceAll(value, `"v":9`, `"v":0`)
+ }
+
+ // klogr doesn't warn about invalid KVs and just inserts
+ // "(MISSING)".
+ for key, value := range map[string]string{
+ `I output.go:] "odd arguments" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:","msg":"odd arguments","v":0,"akey":"avalue","akey2":"(MISSING)"}
+`,
+
+ `I output.go:] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:","msg":"both odd","v":0,"basekey1":"basevar1","basekey2":"(MISSING)","akey":"avalue","akey2":"(MISSING)"}
+`,
+ } {
+ mapping[key] = value
+ }
+
+ newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
+ // Backend: zapr as configured in k8s.io/component-base/logs/json.
+ klog.SetLogger(newZaprLogger(out, v))
+
+ // Frontend: klogr.
+ return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
+ }
+ test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: mapping})
+}
+
+func newZaprLogger(out io.Writer, v int) logr.Logger {
+ encoderConfig := &zapcore.EncoderConfig{
+ MessageKey: "msg",
+ CallerKey: "caller",
+ NameKey: "logger",
+ EncodeDuration: zapcore.StringDurationEncoder,
+ EncodeCaller: zapcore.ShortCallerEncoder,
+ }
+ encoder := zapcore.NewJSONEncoder(*encoderConfig)
+ zapV := -zapcore.Level(v)
+ core := zapcore.NewCore(encoder, zapcore.AddSync(out), zapV)
+ l := zap.New(core, zap.WithCaller(true))
+ logger := zapr.NewLoggerWithOptions(l, zapr.LogInfoLevel("v"), zapr.ErrorKey("err"))
+ return logger
+}
diff --git a/test/output.go b/test/output.go
index a41749140..0c447a827 100644
--- a/test/output.go
+++ b/test/output.go
@@ -67,7 +67,9 @@ func InitKlog() {
// later release.
type OutputConfig struct {
// NewLogger is called to create a new logger. If nil, output via klog
- // is tested. Support for -vmodule is optional.
+ // is tested. Support for -vmodule is optional. ClearLogger is called
+ // after each test, therefore it is okay to user SetLogger without
+ // undoing that in the callback.
NewLogger func(out io.Writer, v int, vmodule string) logr.Logger
// AsBackend enables testing through klog and the logger set there with
@@ -187,10 +189,10 @@ func Output(t *testing.T, config OutputConfig) {
"odd WithValues": {
withValues: []interface{}{"keyWithoutValue"},
moreValues: []interface{}{"anotherKeyWithoutValue"},
- text: "test",
- expectedOutput: `I output.go:] "test" keyWithoutValue="(MISSING)"
-I output.go:] "test" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
-I output.go:] "test" keyWithoutValue="(MISSING)"
+ text: "odd WithValues",
+ expectedOutput: `I output.go:] "odd WithValues" keyWithoutValue="(MISSING)"
+I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
+I output.go:] "odd WithValues" keyWithoutValue="(MISSING)"
`,
},
"multiple WithValues": {
@@ -228,9 +230,9 @@ I output.go:] "test" firstKey=1 secondKey=3
`,
},
"handle odd-numbers of KVs": {
- text: "test",
+ text: "odd arguments",
values: []interface{}{"akey", "avalue", "akey2"},
- expectedOutput: `I output.go:] "test" akey="avalue" akey2="(MISSING)"
+ expectedOutput: `I output.go:] "odd arguments" akey="avalue" akey2="(MISSING)"
`,
},
"html characters": {
@@ -247,9 +249,9 @@ I output.go:] "test" firstKey=1 secondKey=3
},
"handle odd-numbers of KVs in both log values and Info args": {
withValues: []interface{}{"basekey1", "basevar1", "basekey2"},
- text: "test",
+ text: "both odd",
values: []interface{}{"akey", "avalue", "akey2"},
- expectedOutput: `I output.go:] "test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+ expectedOutput: `I output.go:] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
`,
},
"KObj": {
@@ -289,6 +291,8 @@ I output.go:] "test" firstKey=1 secondKey=3
}
for n, test := range tests {
t.Run(n, func(t *testing.T) {
+ defer klog.ClearLogger()
+
printWithLogger := func(logger logr.Logger) {
for _, name := range test.withNames {
logger = logger.WithName(name)
@@ -298,21 +302,21 @@ I output.go:] "test" firstKey=1 secondKey=3
// the combination, then again the original logger.
// It must not have been modified. This produces
// three log entries.
- logger = logger.WithValues(test.withValues...)
+ logger = logger.WithValues(test.withValues...) //
loggers := []logr.Logger{logger}
if test.moreValues != nil {
- loggers = append(loggers, logger.WithValues(test.moreValues...), logger)
+ loggers = append(loggers, logger.WithValues(test.moreValues...), logger) //
}
if test.evenMoreValues != nil {
- loggers = append(loggers, logger.WithValues(test.evenMoreValues...))
+ loggers = append(loggers, logger.WithValues(test.evenMoreValues...)) //
}
for _, logger := range loggers {
if test.withHelper {
- loggerHelper(logger, test.text, test.values)
+ loggerHelper(logger, test.text, test.values) //
} else if test.err != nil {
- logger.Error(test.err, test.text, test.values...)
+ logger.Error(test.err, test.text, test.values...) //
} else {
- logger.V(test.v).Info(test.text, test.values...)
+ logger.V(test.v).Info(test.text, test.values...) //
}
}
}
@@ -394,12 +398,17 @@ I output.go:] "test" firstKey=1 secondKey=3
if repl, ok := config.ExpectedOutputMapping[expected]; ok {
expected = repl
}
+ expectedWithPlaceholder := expected
expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", callLine))
expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-18))
expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-15))
expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", expectedLine-12))
if actual != expected {
- t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expected, actual)
+ if expectedWithPlaceholder == test.expectedOutput {
+ t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expectedWithPlaceholder, actual)
+ } else {
+ t.Errorf("Output mismatch. klog:\n%s\nExpected:\n%s\nActual:\n%s\n", test.expectedOutput, expectedWithPlaceholder, actual)
+ }
}
}
@@ -414,7 +423,6 @@ I output.go:] "test" firstKey=1 secondKey=3
if config.AsBackend {
testOutput(t, printWithKlogLine, func(buffer *bytes.Buffer) {
klog.SetLogger(config.NewLogger(buffer, 10, ""))
- defer klog.ClearLogger()
printWithKlog()
})
return
@@ -645,9 +653,14 @@ I output.go:] "test" firstKey=1 secondKey=3
if repl, ok := config.ExpectedOutputMapping[expected]; ok {
expected = repl
}
+ expectedWithPlaceholder := expected
expected = strings.ReplaceAll(expected, "", fmt.Sprintf("%d", callLine))
if actual != expected {
- t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expected, actual)
+ if expectedWithPlaceholder == test.output {
+ t.Errorf("Output mismatch. Expected:\n%s\nActual:\n%s\n", expectedWithPlaceholder, actual)
+ } else {
+ t.Errorf("Output mismatch. klog:\n%s\nExpected:\n%s\nActual:\n%s\n", test.output, expectedWithPlaceholder, actual)
+ }
}
})
}
diff --git a/test/output_test.go b/test/output_test.go
deleted file mode 100644
index 5a43a1ced..000000000
--- a/test/output_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package test
-
-import (
- "io"
- "testing"
-
- "github.com/go-logr/logr"
-
- "k8s.io/klog/v2/klogr"
- "k8s.io/klog/v2/textlogger"
-)
-
-func init() {
- InitKlog()
-}
-
-// TestKlogOutput tests klog output without a logger.
-func TestKlogOutput(t *testing.T) {
- Output(t, OutputConfig{})
-}
-
-// TestTextloggerOutput tests the textlogger, directly and as backend.
-func TestTextloggerOutput(t *testing.T) {
- newLogger := func(out io.Writer, v int, vmodule string) logr.Logger {
- config := textlogger.NewConfig(
- textlogger.Verbosity(v),
- textlogger.Output(out),
- )
- if err := config.VModule().Set(vmodule); err != nil {
- panic(err)
- }
- return textlogger.NewLogger(config)
- }
- t.Run("direct", func(t *testing.T) {
- Output(t, OutputConfig{NewLogger: newLogger, SupportsVModule: true})
- })
- t.Run("klog-backend", func(t *testing.T) {
- Output(t, OutputConfig{NewLogger: newLogger, AsBackend: true})
- })
-}
-
-// TestKlogrOutput tests klogr output via klog.
-func TestKlogrOutput(t *testing.T) {
- Output(t, OutputConfig{
- NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger {
- return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog))
- },
- })
-}
diff --git a/test/zapr.go b/test/zapr.go
new file mode 100644
index 000000000..0bd94731e
--- /dev/null
+++ b/test/zapr.go
@@ -0,0 +1,246 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+// ZaprOutputMappingDirect provides a mapping from klog output to the
+// corresponding zapr output when zapr is called directly.
+//
+// Experimental
+//
+// Notice: This package is EXPERIMENTAL and may be changed or removed in a
+// later release.
+func ZaprOutputMappingDirect() map[string]string {
+ return map[string]string{
+ `I output.go:] "test" akey="<&>"
+`: `{"caller":"test/output.go:","msg":"test","v":0,"akey":"<&>"}
+`,
+
+ `E output.go:] "test" err="whoops"
+`: `{"caller":"test/output.go:","msg":"test","err":"whoops"}
+`,
+
+ `I output.go:] "helper" akey="avalue"
+`: `{"caller":"test/output.go:","msg":"helper","v":0,"akey":"avalue"}
+`,
+
+ `I output.go:] "hello/world: test" akey="avalue"
+`: `{"logger":"hello.world","caller":"test/output.go:","msg":"test","v":0,"akey":"avalue"}
+`,
+
+ `I output.go:] "test" X="y" duration="1m0s" A="b"
+`: `{"caller":"test/output.go:","msg":"test","duration":"1h0m0s","X":"y","v":0,"duration":"1m0s","A":"b"}
+`,
+
+ `I output.go:] "test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4"
+`: `{"caller":"test/output.go:","msg":"test","akey9":"avalue9","akey8":"avalue8","akey1":"avalue1","v":0,"akey5":"avalue5","akey4":"avalue4"}
+`,
+
+ `I output.go:] "test"
+`: `{"caller":"test/output.go:","msg":"test","v":0}
+`,
+
+ `I output.go:] "\"quoted\"" key="\"quoted value\""
+`: `{"caller":"test/output.go:","msg":"\"quoted\"","v":0,"key":"\"quoted value\""}
+`,
+
+ `I output.go:] "test" err="whoops"
+`: `{"caller":"test/output.go:","msg":"test","v":0,"err":"whoops"}
+`,
+
+ `I output.go:] "test" pod="kube-system/pod-1"
+`: `{"caller":"test/output.go:","msg":"test","v":0,"pod":{"name":"pod-1","namespace":"kube-system"}}
+`,
+
+ `I output.go:] "test" pods=[kube-system/pod-1 kube-system/pod-2]
+`: `{"caller":"test/output.go:","msg":"test","v":0,"pods":[{"name":"pod-1","namespace":"kube-system"},{"name":"pod-2","namespace":"kube-system"}]}
+`,
+
+ `I output.go:] "test" akey="avalue"
+`: `{"caller":"test/output.go:","msg":"test","v":0,"akey":"avalue"}
+`,
+
+ `I output.go:] "me: test" akey="avalue"
+`: `{"logger":"me","caller":"test/output.go:","msg":"test","v":0,"akey":"avalue"}
+`,
+
+ `I output.go:] "test" akey="avalue2"
+`: `{"caller":"test/output.go:","msg":"test","akey":"avalue","v":0,"akey":"avalue2"}
+`,
+
+ `I output.go:] "you see me"
+`: `{"caller":"test/output.go:","msg":"you see me","v":9}
+`,
+
+ `I output.go:] "test" firstKey=1
+I output.go:] "test" firstKey=1 secondKey=2
+I output.go:] "test" firstKey=1
+I output.go:] "test" firstKey=1 secondKey=3
+`: `{"caller":"test/output.go:","msg":"test","firstKey":1,"v":0}
+{"caller":"test/output.go:","msg":"test","firstKey":1,"secondKey":2,"v":0}
+{"caller":"test/output.go:","msg":"test","firstKey":1,"v":0}
+{"caller":"test/output.go:","msg":"test","firstKey":1,"secondKey":3,"v":0}
+`,
+
+ `I output.go:] "odd WithValues" keyWithoutValue="(MISSING)"
+I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" anotherKeyWithoutValue="(MISSING)"
+I output.go:] "odd WithValues" keyWithoutValue="(MISSING)"
+`: `{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","ignored key":"keyWithoutValue"}
+{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","ignored key":"anotherKeyWithoutValue"}
+{"caller":"test/output.go:","msg":"odd WithValues","v":0}
+{"caller":"test/output.go:","msg":"odd WithValues","v":0}
+{"caller":"test/output.go:","msg":"odd WithValues","v":0}
+`,
+
+ `I output.go:] "odd arguments" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","ignored key":"akey2"}
+{"caller":"test/output.go:","msg":"odd arguments","v":0,"akey":"avalue"}
+`,
+
+ `I output.go:] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","ignored key":"basekey2"}
+{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","basekey1":"basevar1","ignored key":"akey2"}
+{"caller":"test/output.go:","msg":"both odd","basekey1":"basevar1","v":0,"akey":"avalue"}
+`,
+
+ // klog.Info
+ `I output.go: