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:] "helloworld\n" +`: `{"caller":"test/output.go:","msg":"helloworld\n","v":0} +`, + + // klog.Infoln + `I output.go:] "hello world\n" +`: `{"caller":"test/output.go:","msg":"hello world\n","v":0} +`, + + // klog.Error + `E output.go:] "helloworld\n" +`: `{"caller":"test/output.go:","msg":"helloworld\n"} +`, + + // klog.Errorln + `E output.go:] "hello world\n" +`: `{"caller":"test/output.go:","msg":"hello world\n"} +`, + + // klog.ErrorS + `E output.go:] "world" err="hello" +`: `{"caller":"test/output.go:","msg":"world","err":"hello"} +`, + + // klog.InfoS + `I output.go:] "hello" what="world" +`: `{"caller":"test/output.go:","msg":"hello","v":0,"what":"world"} +`, + + // klog.V(1).Info + `I output.go:] "hellooneworld\n" +`: `{"caller":"test/output.go:","msg":"hellooneworld\n","v":1} +`, + + // klog.V(1).Infoln + `I output.go:] "hello one world\n" +`: `{"caller":"test/output.go:","msg":"hello one world\n","v":1} +`, + + // klog.V(1).ErrorS + `E output.go:] "one world" err="hello" +`: `{"caller":"test/output.go:","msg":"one world","err":"hello"} +`, + + // klog.V(1).InfoS + `I output.go:] "hello" what="one world" +`: `{"caller":"test/output.go:","msg":"hello","v":1,"what":"one world"} +`, + } +} + +// ZaprOutputMappingIndirect provides a mapping from klog output to the +// corresponding zapr output when zapr is called indirectly through +// klog. +// +// This is different from ZaprOutputMappingDirect because: +// - WithName gets added to the message by Output. +// - zap uses . as separator instead of / between WithName values, +// here we get slashes because Output concatenates these values. +// - WithValues are added to the normal key/value parameters by +// Output, which puts them after "v". +// - Output does that without emitting the warning that we get +// from zapr. +// - zap drops keys with missing values, here we get "(MISSING)". +// - zap does not de-duplicate key/value pairs, here klog does that +// for it. +// +// Experimental +// +// Notice: This package is EXPERIMENTAL and may be changed or removed in a +// later release. +func ZaprOutputMappingIndirect() map[string]string { + mapping := ZaprOutputMappingDirect() + + for key, value := range map[string]string{ + `I output.go:] "hello/world: test" akey="avalue" +`: `{"caller":"test/output.go:","msg":"hello/world: test","v":0,"akey":"avalue"} +`, + + `I output.go:] "me: test" akey="avalue" +`: `{"caller":"test/output.go:","msg":"me: test","v":0,"akey":"avalue"} +`, + + `I output.go:] "odd parameters" 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":"akey2"} +{"caller":"test/output.go:","msg":"odd parameters","v":0,"basekey1":"basevar1","basekey2":"(MISSING)","akey":"avalue"} +`, + + `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 WithValues","v":0,"keyWithoutValue":"(MISSING)"} +{"caller":"test/output.go:","msg":"odd WithValues","v":0,"keyWithoutValue":"(MISSING)","anotherKeyWithoutValue":"(MISSING)"} +{"caller":"test/output.go:","msg":"odd WithValues","v":0,"keyWithoutValue":"(MISSING)"} +`, + + `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":"akey2"} +{"caller":"test/output.go:","msg":"both odd","v":0,"basekey1":"basevar1","basekey2":"(MISSING)","akey":"avalue"} +`, + + `I output.go:] "test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4" +`: `{"caller":"test/output.go:","msg":"test","v":0,"akey9":"avalue9","akey8":"avalue8","akey1":"avalue1","akey5":"avalue5","akey4":"avalue4"} +`, + + `I output.go:] "test" akey="avalue2" +`: `{"caller":"test/output.go:","msg":"test","v":0,"akey":"avalue2"} +`, + + `I output.go:] "test" X="y" duration="1m0s" A="b" +`: `{"caller":"test/output.go:","msg":"test","v":0,"X":"y","duration":"1m0s","A":"b"} +`, + + `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","v":0,"firstKey":1} +{"caller":"test/output.go:","msg":"test","v":0,"firstKey":1,"secondKey":2} +{"caller":"test/output.go:","msg":"test","v":0,"firstKey":1} +{"caller":"test/output.go:","msg":"test","v":0,"firstKey":1,"secondKey":3} +`, + } { + mapping[key] = value + } + return mapping +} From a58a994786e162447603bc62da102d2eb77c5c92 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 14 Feb 2022 19:17:50 +0100 Subject: [PATCH 042/125] klogr: pass verbosity level through to klog That wasn't (and still isn't) necessary when klog itself emits the log entry because it doesn't log the verbosity. But when SetLogger was used to enabled logging through a backend logger, that logger may care about the verbosity level. --- examples/output_test/output_test.go | 5 ----- klogr/klogr.go | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index a6ef0abb2..0624e3a74 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -21,7 +21,6 @@ package output_test import ( "io" - "strings" "testing" "github.com/go-logr/logr" @@ -112,10 +111,6 @@ func TestKlogrStackText(t *testing.T) { // 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)". diff --git a/klogr/klogr.go b/klogr/klogr.go index 02433e844..8206c10d4 100644 --- a/klogr/klogr.go +++ b/klogr/klogr.go @@ -122,13 +122,13 @@ func (l klogger) Info(level int, msg string, kvList ...interface{}) { trimmed := serialize.TrimDuplicates(l.values, kvList) fixedStr := flatten(trimmed[0]...) userStr := flatten(trimmed[1]...) - klog.InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr) + klog.V(klog.Level(level)).InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr) case FormatKlog: trimmed := serialize.TrimDuplicates(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - klog.InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...) + klog.V(klog.Level(level)).InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...) } } From 7dac793516762ac4783b8ebb5ffb357cec4976e4 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 15 Feb 2022 19:31:40 +0100 Subject: [PATCH 043/125] github: split up test steps Splitting up the tests ensures that each step runs in the right directory without having to rely on more complex shell scripting. That failures get reported separately is an added bonus. The simpler `(cd hack/tools && go test ./...)` wasn't supported by Windows. --- .github/workflows/test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cebf8602f..5ff4d8999 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,12 +14,14 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - - name: Test + - name: Test klog run: | go get -t -v ./... go test -v -race ./... - cd hack/tools && go test -v -race ./... && cd ../.. - cd examples && go test -v -race ./... && cd .. + - name: Test hack/tools + run: cd hack/tools && go test -v -race ./... + - name: Test examples + run: cd examples && go test -v -race ./... lint: runs-on: ubuntu-latest steps: From 6f34ff9b4c4fcb773f4727f7c83475fadd1f52b8 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Feb 2022 13:00:08 +0100 Subject: [PATCH 044/125] test: fix Go version matrix The matrix didn't really have an effect, all tests ran with the same (default?) Go version. For example, the "1.17.x, ubuntu-latest" test https://github.com/kubernetes/klog/runs/5215345316?check_suite_focus=true for https://github.com/kubernetes/klog/pull/297 complained about functions that are available in 1.17: Error: logcheck/pkg/filter_test.go:28:12: undefined: os.WriteFile Error: logcheck/pkg/filter_test.go:132:14: undefined: os.WriteFile --- .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 5ff4d8999..38793a372 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, 1.17.x] + go-version: [1.14, 1.15, 1.16, 1.17] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From 708ae6185502efd239742ffd2e04031c6e1e5551 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 15 Feb 2022 12:57:24 +0100 Subject: [PATCH 045/125] handle panics in MarshalLog, Error, String The previous fix only covered fmt.Stringer.String in klog, but not klogr. error.Error and logr.Marshaler.MarshalLog have the same problem. The replacement string now captures the error, which makes it consistent with https://github.com/go-logr/logr/pull/130. Two different corner cases may be handled differently: - panic for a nil object - panic for a valid object Only zapr v1.2.3 handles the panic in MarshalLog. --- examples/go.mod | 2 +- examples/go.sum | 2 + internal/serialize/keyvalues.go | 22 +++++++-- internal/serialize/keyvalues_test.go | 2 +- klogr/klogr.go | 2 +- test/output.go | 74 ++++++++++++++++++++++++++++ test/zapr.go | 25 ++++++++++ 7 files changed, 122 insertions(+), 7 deletions(-) diff --git a/examples/go.mod b/examples/go.mod index d7956666e..9c1648451 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/go-logr/logr v1.2.2 - github.com/go-logr/zapr v1.2.2 + github.com/go-logr/zapr v1.2.3 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 5b43f393e..b7fda524e 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -8,6 +8,8 @@ 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/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/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= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index 4f583912f..d89731368 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -126,11 +126,11 @@ 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, stringerToString(v)) + writeStringValue(b, true, StringerToString(v)) case string: writeStringValue(b, true, v) case error: - writeStringValue(b, true, v.Error()) + writeStringValue(b, true, ErrorToString(v)) case []byte: // In https://github.com/kubernetes/klog/pull/237 it was decided // to format byte slices with "%+q". The advantages of that are: @@ -151,16 +151,30 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { } } -func stringerToString(s fmt.Stringer) (ret string) { +// StringerToString converts a Stringer to a string, +// handling panics if they occur. +func StringerToString(s fmt.Stringer) (ret string) { defer func() { if err := recover(); err != nil { - ret = "nil" + ret = fmt.Sprintf("", err) } }() ret = s.String() return } +// ErrorToString converts an error to a string, +// handling panics if they occur. +func ErrorToString(err error) (ret string) { + defer func() { + if err := recover(); err != nil { + ret = fmt.Sprintf("", err) + } + }() + ret = err.Error() + return +} + func writeStringValue(b *bytes.Buffer, quote bool, v string) { data := []byte(v) index := bytes.IndexByte(data, '\n') diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index f7fdaba36..affb887ec 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -137,7 +137,7 @@ No whitespace.`, }, { keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint}, - want: " point-1=\"x=100, y=200\" point-2=\"nil\"", + want: " point-1=\"x=100, y=200\" point-2=\"\"", }, } diff --git a/klogr/klogr.go b/klogr/klogr.go index 8206c10d4..48cb65815 100644 --- a/klogr/klogr.go +++ b/klogr/klogr.go @@ -140,7 +140,7 @@ func (l klogger) Error(err error, msg string, kvList ...interface{}) { msgStr := flatten("msg", msg) var loggableErr interface{} if err != nil { - loggableErr = err.Error() + loggableErr = serialize.ErrorToString(err) } switch l.format { case FormatSerialize: diff --git a/test/output.go b/test/output.go index 0c447a827..bec12018d 100644 --- a/test/output.go +++ b/test/output.go @@ -286,6 +286,42 @@ I output.go:] "test" firstKey=1 secondKey=3 text: "test", err: errors.New("whoops"), expectedOutput: `E output.go:] "test" err="whoops" +`, + }, + "Error() for nil": { + text: "error nil", + err: (*customErrorJSON)(nil), + expectedOutput: `E output.go:] "error nil" err="" +`, + }, + "String() for nil": { + text: "stringer nil", + values: []interface{}{"stringer", (*stringer)(nil)}, + expectedOutput: `I output.go:] "stringer nil" stringer="" +`, + }, + "MarshalLog() for nil": { + text: "marshaler nil", + values: []interface{}{"obj", (*klog.ObjectRef)(nil)}, + expectedOutput: `I output.go:] "marshaler nil" obj="" +`, + }, + "Error() that panics": { + text: "error panic", + err: faultyError{}, + expectedOutput: `E output.go:] "error panic" err="" +`, + }, + "String() that panics": { + text: "stringer panic", + values: []interface{}{"stringer", faultyStringer{}}, + expectedOutput: `I output.go:] "stringer panic" stringer="" +`, + }, + "MarshalLog() that panics": { + text: "marshaler panic", + values: []interface{}{"obj", faultyMarshaler{}}, + expectedOutput: `I output.go:] "marshaler panic" obj={} `, }, } @@ -699,3 +735,41 @@ func (e *customErrorJSON) Error() string { func (e *customErrorJSON) MarshalJSON() ([]byte, error) { return json.Marshal(strings.ToUpper(e.s)) } + +type stringer struct { + s string +} + +// String crashes when called for nil. +func (s *stringer) String() string { + return s.s +} + +var _ fmt.Stringer = &stringer{} + +type faultyStringer struct{} + +// String always panics. +func (f faultyStringer) String() string { + panic("fake String panic") +} + +var _ fmt.Stringer = faultyStringer{} + +type faultyMarshaler struct{} + +// MarshalLog always panics. +func (f faultyMarshaler) MarshalLog() interface{} { + panic("fake MarshalLog panic") +} + +var _ logr.Marshaler = faultyMarshaler{} + +type faultyError struct{} + +// Error always panics. +func (f faultyError) Error() string { + panic("fake Error panic") +} + +var _ error = faultyError{} diff --git a/test/zapr.go b/test/zapr.go index 0bd94731e..20b118aad 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -114,6 +114,31 @@ 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":"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"} +`, + + `I output.go:] "marshaler nil" obj="" +`: `{"caller":"test/output.go:","msg":"marshaler nil","v":0,"objError":"PANIC=value method k8s.io/klog/v2.ObjectRef.MarshalLog called using nil *ObjectRef pointer"} +`, + + // zap replaces a panic for a nil object with . + `E output.go:] "error nil" err="" +`: `{"caller":"test/output.go:","msg":"error nil","err":""} +`, + + `I output.go:] "stringer nil" stringer="" +`: `{"caller":"test/output.go:","msg":"stringer nil","v":0,"stringer":""} +`, + + `I output.go:] "stringer panic" stringer="" +`: `{"caller":"test/output.go:","msg":"stringer panic","v":0,"stringerError":"PANIC=fake String panic"} +`, + + `E output.go:] "error panic" err="" +`: `{"caller":"test/output.go:","msg":"error panic","errError":"PANIC=fake Error panic"} +`, + + `I output.go:] "marshaler panic" obj={} +`: `{"caller":"test/output.go:","msg":"marshaler panic","v":0,"objError":"PANIC=fake MarshalLog panic"} `, // klog.Info From 7ea6d6adb64592611d05066e927aaec7d2d20b16 Mon Sep 17 00:00:00 2001 From: katexochen <49727155+katexochen@users.noreply.github.com> Date: Sun, 27 Feb 2022 10:15:18 -1100 Subject: [PATCH 046/125] Fix goroutine leak: make flushDaemon stoppable (#293) * Make flushDaemon stoppable and testable Refactor flushDaemon function into a seperate struct. Instead of starting the daemon in the init function, it is now started during createFile, where it is only used. Add function to stop daemon. This way, users can gracefully shut down klog and prevent goroutine leakage. Test daemon flushing. * fixup! Make flushDaemon stoppable and testable * fixup! Make flushDaemon stoppable and testable * fixup! Make flushDaemon stoppable and testable * fixup! Make flushDaemon stoppable and testable Co-authored-by: katexochen --- examples/flushing/flushing_test.go | 39 +++++++++++++ examples/go.mod | 1 + examples/go.sum | 48 +++++++++++++++- go.mod | 5 +- go.sum | 11 ++++ klog.go | 90 ++++++++++++++++++++++++++++-- klog_test.go | 83 ++++++++++++++++++++++++--- 7 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 examples/flushing/flushing_test.go diff --git a/examples/flushing/flushing_test.go b/examples/flushing/flushing_test.go new file mode 100644 index 000000000..3d46fbcab --- /dev/null +++ b/examples/flushing/flushing_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "testing" + + "go.uber.org/goleak" + "k8s.io/klog/v2" +) + +func main() { + klog.InitFlags(nil) + + // By default klog writes to stderr. Setting logtostderr to false makes klog + // write to a log file. + flag.Set("logtostderr", "false") + flag.Set("log_file", "myfile.log") + flag.Parse() + + // Info writes the first log message. When the first log file is created, + // a flushDaemon is started to frequently flush bytes to the file. + klog.Info("nice to meet you") + + // klog won't ever stop this flushDaemon. To exit without leaking a goroutine, + // the daemon can be stopped manually. + klog.StopFlushDaemon() + + // After you stopped the flushDaemon, you can still manually flush. + klog.Info("bye") + klog.Flush() +} + +func TestLeakingFlushDaemon(t *testing.T) { + // goleak detects leaking goroutines. + defer goleak.VerifyNone(t) + + // Without calling StopFlushDaemon in main, this test will fail. + main() +} diff --git a/examples/go.mod b/examples/go.mod index 9c1648451..b7750acfb 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-logr/logr v1.2.2 github.com/go-logr/zapr v1.2.3 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + go.uber.org/goleak v1.1.12 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 b7fda524e..3326ef475 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,6 +1,7 @@ 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.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= @@ -21,38 +22,81 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 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/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-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= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/go.mod b/go.mod index 31aefba74..6385c6cde 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module k8s.io/klog/v2 go 1.13 -require github.com/go-logr/logr v1.2.0 +require ( + github.com/go-logr/logr v1.2.0 + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 +) diff --git a/go.sum b/go.sum index 919fbadbc..ef731394a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,13 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +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= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/klog.go b/klog.go index 3435461d8..0b235f42f 100644 --- a/klog.go +++ b/klog.go @@ -94,6 +94,7 @@ import ( "k8s.io/klog/v2/internal/buffer" "k8s.io/klog/v2/internal/serialize" "k8s.io/klog/v2/internal/severity" + "k8s.io/utils/clock" ) // severityValue identifies the sort of log: info, warning etc. It also implements @@ -380,7 +381,7 @@ type flushSyncWriter interface { io.Writer } -// init sets up the defaults and runs flushDaemon. +// init sets up the defaults. func init() { logging.stderrThreshold = severityValue{ Severity: severity.ErrorLog, // Default stderrThreshold is ERROR. @@ -395,7 +396,7 @@ func init() { logging.addDirHeader = false logging.skipLogHeaders = false logging.oneOutput = false - go logging.flushDaemon() + logging.flushD = newFlushDaemon(flushInterval, logging.lockAndFlushAll, nil) } // InitFlags is for explicitly initializing the flags. @@ -447,6 +448,8 @@ type loggingT struct { mu sync.Mutex // file holds writer for each of the log types. file [severity.NumSeverity]flushSyncWriter + // flushD holds a flushDaemon that frequently flushes log file buffers. + flushD *flushDaemon // 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. @@ -1022,6 +1025,7 @@ 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.Severity) error { + l.flushD.run() now := time.Now() // Files are created in decreasing severity order, so as soon as we find one // has already been created, we can stop. @@ -1042,10 +1046,86 @@ func (l *loggingT) createFiles(sev severity.Severity) error { const flushInterval = 5 * time.Second // flushDaemon periodically flushes the log file buffers. -func (l *loggingT) flushDaemon() { - for range time.NewTicker(flushInterval).C { - l.lockAndFlushAll() +type flushDaemon struct { + mu sync.Mutex + clock clock.WithTicker + interval time.Duration + flush func() + stopC chan struct{} + stopDone chan struct{} +} + +// newFlushDaemon returns a new flushDaemon. If the passed clock is nil, a +// clock.RealClock is used. +func newFlushDaemon(interval time.Duration, flush func(), tickClock clock.WithTicker) *flushDaemon { + if tickClock == nil { + tickClock = clock.RealClock{} } + return &flushDaemon{ + interval: interval, + flush: flush, + clock: tickClock, + } +} + +// run starts a goroutine that periodically calls the daemons flush function. +// Calling run on an already running daemon will have no effect. +func (f *flushDaemon) run() { + f.mu.Lock() + defer f.mu.Unlock() + + if f.stopC != nil { // daemon already running + return + } + + f.stopC = make(chan struct{}, 1) + f.stopDone = make(chan struct{}, 1) + + ticker := f.clock.NewTicker(f.interval) + go func() { + defer ticker.Stop() + defer func() { f.stopDone <- struct{}{} }() + for { + select { + case <-ticker.C(): + f.flush() + case <-f.stopC: + f.flush() + return + } + } + }() +} + +// stop stops the running flushDaemon and waits until the daemon has shut down. +// Calling stop on a daemon that isn't running will have no effect. +func (f *flushDaemon) stop() { + f.mu.Lock() + defer f.mu.Unlock() + + if f.stopC == nil { // daemon not running + return + } + + f.stopC <- struct{}{} + <-f.stopDone + + f.stopC = nil + f.stopDone = nil +} + +// isRunning returns true if the flush daemon is running. +func (f *flushDaemon) isRunning() bool { + f.mu.Lock() + defer f.mu.Unlock() + return f.stopC != nil +} + +// StopFlushDaemon stops the flush daemon, if running. +// This prevents klog from leaking goroutines on shutdown. After stopping +// the daemon, you can still manually flush buffers by calling Flush(). +func StopFlushDaemon() { + logging.flushD.stop() } // lockAndFlushAll is like flushAll but locks l.mu first. diff --git a/klog_test.go b/klog_test.go index 6e1667639..95553d5f4 100644 --- a/klog_test.go +++ b/klog_test.go @@ -39,6 +39,7 @@ import ( "k8s.io/klog/v2/internal/buffer" "k8s.io/klog/v2/internal/severity" "k8s.io/klog/v2/internal/test" + testingclock "k8s.io/utils/clock/testing" ) // TODO: This test package should be refactored so that tests cannot @@ -378,10 +379,11 @@ func TestSetOutputDataRace(t *testing.T) { setFlags() defer logging.swap(logging.newBuffers()) var wg sync.WaitGroup + var daemons []*flushDaemon for i := 1; i <= 50; i++ { - go func() { - logging.flushDaemon() - }() + daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) + daemon.run() + daemons = append(daemons, daemon) } for i := 1; i <= 50; i++ { wg.Add(1) @@ -391,9 +393,9 @@ func TestSetOutputDataRace(t *testing.T) { }() } for i := 1; i <= 50; i++ { - go func() { - logging.flushDaemon() - }() + daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) + daemon.run() + daemons = append(daemons, daemon) } for i := 1; i <= 50; i++ { wg.Add(1) @@ -403,11 +405,14 @@ func TestSetOutputDataRace(t *testing.T) { }() } for i := 1; i <= 50; i++ { - go func() { - logging.flushDaemon() - }() + daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) + daemon.run() + daemons = append(daemons, daemon) } wg.Wait() + for _, d := range daemons { + d.stop() + } } func TestLogToOutput(t *testing.T) { @@ -1852,3 +1857,63 @@ func (s *structWithLock) addWithDefer() { defer s.m.Unlock() s.n++ } + +func TestFlushDaemon(t *testing.T) { + for sev := severity.InfoLog; sev < severity.FatalLog; sev++ { + flushed := make(chan struct{}, 1) + spyFunc := func() { + flushed <- struct{}{} + } + testClock := testingclock.NewFakeClock(time.Now()) + testLog := loggingT{ + flushD: newFlushDaemon(time.Second, spyFunc, testClock), + } + + // Calling testLog will call createFile, which should start the daemon. + testLog.print(sev, nil, nil, "x") + + if !testLog.flushD.isRunning() { + t.Error("expected flushD to be running") + } + + timer := time.NewTimer(10 * time.Second) + defer timer.Stop() + testClock.Step(time.Second) + select { + case <-flushed: + case <-timer.C: + t.Fatal("flushDaemon didn't call flush function on tick") + } + + timer = time.NewTimer(10 * time.Second) + defer timer.Stop() + testClock.Step(time.Second) + select { + case <-flushed: + case <-timer.C: + t.Fatal("flushDaemon didn't call flush function on second tick") + } + + timer = time.NewTimer(10 * time.Second) + defer timer.Stop() + testLog.flushD.stop() + select { + case <-flushed: + case <-timer.C: + t.Fatal("flushDaemon didn't call flush function one last time on stop") + } + } +} + +func TestStopFlushDaemon(t *testing.T) { + logging.flushD.stop() + logging.flushD = newFlushDaemon(time.Second, func() {}, nil) + logging.flushD.run() + if !logging.flushD.isRunning() { + t.Error("expected flushD to be running") + } + StopFlushDaemon() + if logging.flushD.isRunning() { + t.Error("expected flushD to be stopped") + } +} From 07fe24617c36a733c3658831c4dd1d1088330345 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 27 May 2021 15:15:50 +0200 Subject: [PATCH 047/125] testinglogger: per-test, structured logging This logr implementation can be used in tests to ensure that output is associated with the currently running test. Compared to using the global klog instance in a standard Go test that has some advantages: - Log messages are associated with the currently running test. Tests can run in parallel without interleaving their output. - Log output is only printed when a test fails, unless "go test -v" is used. - Because of that, the logger can print more log messages by default, with 5 being used because that is recommended for debugging in https://github.com/kubernetes/community/blob/fbc5ca53d6bfd8388b335f7d0198b67a14b99d91/contributors/devel/sig-instrumentation/logging.md?plain=1#L34-L36 That threshold can be changed via -testing.v after registering that flag by importing k8s.io/klog/v2/ktesting/init. The motivation for hosting this logger in klog is that it shares the formatting code with klogr. Conceptually this is identical to go-logr/logr/testing, just the output is different (klog text format vs. JSON). $ go test -v ./testinglogger/example/ === RUN TestKlog example_test.go:69: INFO hello world example_test.go:70: ERROR failed err="failed: some error" example_test.go:71: INFO verbosity 1 example_test.go:72: INFO main/helper: with prefix example_test.go:73: INFO key/value pairs int=1 float=2 pair="(1, 2)" kobj="kube-system/sally" --- PASS: TestKlog (0.00s) The corresponding output from go-logr/logr/testing would be: === RUN TestLogr example_test.go:69: "ts"="2021-09-07 16:44:54.307551" "level"=0 "msg"="hello world" example_test.go:70: "ts"="2021-09-07 16:44:54.307664" "msg"="failed" "error"="failed: some error" example_test.go:71: "ts"="2021-09-07 16:44:54.307686" "level"=1 "msg"="verbosity 1" example_test.go:72: main/helper: "ts"="2021-09-07 16:44:54.307703" "level"=0 "msg"="with prefix" example_test.go:73: "ts"="2021-09-07 16:44:54.307733" "level"=0 "msg"="key/value pairs" "int"=1 "float"=2 "pair"={} "kobj"={"name":"sally","namespace":"kube-system"} --- PASS: TestLogr (0.00s) --- ktesting/example/example_test.go | 60 +++++++++++++ ktesting/init/init.go | 35 ++++++++ ktesting/options.go | 130 +++++++++++++++++++++++++++ ktesting/setup.go | 48 ++++++++++ ktesting/testinglogger.go | 139 +++++++++++++++++++++++++++++ ktesting/testinglogger_test.go | 147 +++++++++++++++++++++++++++++++ 6 files changed, 559 insertions(+) create mode 100644 ktesting/example/example_test.go create mode 100644 ktesting/init/init.go create mode 100644 ktesting/options.go create mode 100644 ktesting/setup.go create mode 100644 ktesting/testinglogger.go create mode 100644 ktesting/testinglogger_test.go diff --git a/ktesting/example/example_test.go b/ktesting/example/example_test.go new file mode 100644 index 000000000..79eeef447 --- /dev/null +++ b/ktesting/example/example_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 The Kubernetes Authors. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package example + +import ( + "fmt" + "testing" + + "k8s.io/klog/v2" + "k8s.io/klog/v2/internal/test" + "k8s.io/klog/v2/ktesting" + _ "k8s.io/klog/v2/ktesting/init" // add command line flags +) + +func TestKlogr(t *testing.T) { + logger, _ := ktesting.NewTestContext(t) + exampleOutput(logger) +} + +type pair struct { + a, b int +} + +func (p pair) String() string { + return fmt.Sprintf("(%d, %d)", p.a, p.b) +} + +var _ fmt.Stringer = pair{} + +type err struct { + msg string +} + +func (e err) Error() string { + return "failed: " + e.msg +} + +var _ error = err{} + +func exampleOutput(logger klog.Logger) { + logger.Info("hello world") + logger.Error(err{msg: "some error"}, "failed") + logger.V(1).Info("verbosity 1") + logger.WithName("main").WithName("helper").Info("with prefix") + obj := test.KMetadataMock{Name: "joe", NS: "kube-system"} + logger.Info("key/value pairs", + "int", 1, + "float", 2.0, + "pair", pair{a: 1, b: 2}, + "raw", obj, + "kobj", klog.KObj(obj), + ) + logger.V(4).Info("info message level 4") + logger.V(5).Info("info message level 5") + logger.V(6).Info("info message level 6") +} diff --git a/ktesting/init/init.go b/ktesting/init/init.go new file mode 100644 index 000000000..c1fa145a8 --- /dev/null +++ b/ktesting/init/init.go @@ -0,0 +1,35 @@ +/* +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 init registers the command line flags for k8s.io/klogr/testing in +// the flag.CommandLine. This is done during initialization, so merely +// importing it is enough. +// +// Experimental +// +// Notice: This package is EXPERIMENTAL and may be changed or removed in a +// later release. +package init + +import ( + "flag" + + "k8s.io/klog/v2/ktesting" +) + +func init() { + ktesting.DefaultConfig.AddFlags(flag.CommandLine) +} diff --git a/ktesting/options.go b/ktesting/options.go new file mode 100644 index 000000000..8cb13c406 --- /dev/null +++ b/ktesting/options.go @@ -0,0 +1,130 @@ +/* +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 ktesting + +import ( + "flag" + "strconv" + + "k8s.io/klog/v2/internal/verbosity" +) + +// Config influences logging in a test 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 { + vstate *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 +} + +// VerbosityFlagName overrides the default -testing.v for the verbosity level. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func VerbosityFlagName(name string) ConfigOption { + return func(co *configOptions) { + co.verbosityFlagName = name + } +} + +// VModulFlagName overrides the default -testing.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 5. That default is higher +// than in klog itself because it enables logging entries for "the steps +// leading up to errors and warnings" and "troubleshooting" (see +// https://github.com/kubernetes/community/blob/9406b4352fe2d5810cb21cc3cb059ce5886de157/contributors/devel/sig-instrumentation/logging.md#logging-conventions), +// which is useful when debugging a failed test. `go test` only shows the log +// output for failed tests. To see all output, use `go test -v`. +// +// 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 + } +} + +// 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{ + co: configOptions{ + verbosityFlagName: "testing.v", + vmoduleFlagName: "testing.vmodule", + verbosityDefault: 5, + }, + } + for _, opt := range opts { + opt(&c.co) + } + + c.vstate = verbosity.New() + c.vstate.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.vstate.V(), c.co.verbosityFlagName, "number for the log level verbosity of the testing logger") + fs.Var(c.vstate.VModule(), c.co.vmoduleFlagName, "comma-separated list of pattern=N log level settings for files matching the patterns") +} diff --git a/ktesting/setup.go b/ktesting/setup.go new file mode 100644 index 000000000..d3029ae96 --- /dev/null +++ b/ktesting/setup.go @@ -0,0 +1,48 @@ +/* +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 ktesting + +import ( + "context" + + "github.com/go-logr/logr" +) + +// DefaultConfig is the global default logging configuration for a unit +// test. It is used by NewTestContext and k8s.io/klogr/testing/init. +// +// Experimental +// +// Notice: This variable is EXPERIMENTAL and may be changed or removed in a +// later release. +var DefaultConfig = NewConfig() + +// NewTestContext returns a logger and context for use in a unit test case or +// benchmark. The tl parameter can be a testing.T or testing.B pointer that +// will receive all log output. Importing k8s.io/klogr/testing/init will add +// command line flags that modify the configuration of that log output. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func NewTestContext(tl TL) (logr.Logger, context.Context) { + logger := NewLogger(tl, DefaultConfig) + ctx := logr.NewContext(context.Background(), logger) + return logger, ctx + +} diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go new file mode 100644 index 000000000..5269f89d2 --- /dev/null +++ b/ktesting/testinglogger.go @@ -0,0 +1,139 @@ +/* +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 testinglogger contains an implementation of the logr interface +// which is logging through a function like testing.TB.Log function. +// Therefore it can be used in standard Go tests and Gingko test suites +// to ensure that output is associated with the currently running test. +// +// Serialization of the structured log parameters is done in the same way +// as for klog.InfoS. +// +// Experimental +// +// Notice: This package is EXPERIMENTAL and may be changed or removed in a +// later release. +package ktesting + +import ( + "bytes" + + "github.com/go-logr/logr" + + "k8s.io/klog/v2/internal/serialize" + "k8s.io/klog/v2/internal/verbosity" +) + +// TL is the relevant subset of testing.TB. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type TL interface { + Helper() + Log(args ...interface{}) +} + +// NewLogger constructs a new logger for the given test interface. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +func NewLogger(t TL, c *Config) logr.Logger { + return logr.New(&tlogger{ + t: t, + prefix: "", + values: nil, + config: c, + }) +} + +type tlogger struct { + t TL + prefix string + values []interface{} + config *Config +} + +func (l *tlogger) Init(info logr.RuntimeInfo) { +} + +func (l *tlogger) GetCallStackHelper() func() { + return l.t.Helper +} + +func (l *tlogger) Info(level int, msg string, kvList ...interface{}) { + l.t.Helper() + buffer := &bytes.Buffer{} + trimmed := serialize.TrimDuplicates(l.values, kvList) + serialize.KVListFormat(buffer, trimmed[0]...) + serialize.KVListFormat(buffer, trimmed[1]...) + l.log("INFO", msg, buffer) +} + +func (l *tlogger) Enabled(level int) bool { + return l.config.vstate.Enabled(verbosity.Level(level), 1) +} + +func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { + l.t.Helper() + buffer := &bytes.Buffer{} + if err != nil { + serialize.KVListFormat(buffer, "err", err) + } + trimmed := serialize.TrimDuplicates(l.values, kvList) + serialize.KVListFormat(buffer, trimmed[0]...) + serialize.KVListFormat(buffer, trimmed[1]...) + l.log("ERROR", msg, buffer) +} + +func (l *tlogger) log(what, msg string, buffer *bytes.Buffer) { + l.t.Helper() + args := []interface{}{what} + if l.prefix != "" { + args = append(args, l.prefix+":") + } + args = append(args, msg) + if buffer.Len() > 0 { + // Skip leading space inserted by serialize.KVListFormat. + args = append(args, string(buffer.Bytes()[1:])) + } + l.t.Log(args...) +} + +// 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.CallStackHelperLogSink = &tlogger{} diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go new file mode 100644 index 000000000..430ece7c5 --- /dev/null +++ b/ktesting/testinglogger_test.go @@ -0,0 +1,147 @@ +/* +Copyright 2019 The Kubernetes Authors. +Copyright 2020 Intel Coporation. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ktesting + +import ( + "bytes" + "errors" + "fmt" + "testing" +) + +func TestInfo(t *testing.T) { + tests := map[string]struct { + text string + withValues []interface{} + keysAndValues []interface{} + names []string + err error + expectedOutput string + }{ + "should log with values passed to keysAndValues": { + text: "test", + keysAndValues: []interface{}{"akey", "avalue"}, + expectedOutput: `INFO test akey="avalue" +`, + }, + "should support single name": { + names: []string{"hello"}, + text: "test", + keysAndValues: []interface{}{"akey", "avalue"}, + expectedOutput: `INFO hello: test akey="avalue" +`, + }, + "should support multiple names": { + names: []string{"hello", "world"}, + text: "test", + keysAndValues: []interface{}{"akey", "avalue"}, + expectedOutput: `INFO hello/world: test akey="avalue" +`, + }, + "should not print duplicate keys with the same value": { + text: "test", + keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"}, + expectedOutput: `INFO test akey="avalue" +`, + }, + "should only print the last duplicate key when the values are passed to Info": { + text: "test", + keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"}, + expectedOutput: `INFO test akey="avalue2" +`, + }, + "should only print the duplicate key that is passed to Info if one was passed to the logger": { + withValues: []interface{}{"akey", "avalue"}, + text: "test", + keysAndValues: []interface{}{"akey", "avalue"}, + expectedOutput: `INFO test akey="avalue" +`, + }, + "should only print the key passed to Info when one is already set on the logger": { + withValues: []interface{}{"akey", "avalue"}, + text: "test", + keysAndValues: []interface{}{"akey", "avalue2"}, + expectedOutput: `INFO test akey="avalue2" +`, + }, + "should correctly handle odd-numbers of KVs": { + text: "test", + keysAndValues: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `INFO test akey="avalue" akey2="(MISSING)" +`, + }, + "should correctly html characters": { + text: "test", + keysAndValues: []interface{}{"akey", "<&>"}, + expectedOutput: `INFO test akey="<&>" +`, + }, + "should correctly handle odd-numbers of KVs in both log values and Info args": { + withValues: []interface{}{"basekey1", "basevar1", "basekey2"}, + text: "test", + keysAndValues: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `INFO test basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" +`, + }, + "should correctly print regular error types": { + text: "test", + keysAndValues: []interface{}{"err", errors.New("whoops")}, + expectedOutput: `INFO test err="whoops" +`, + }, + "should correctly print regular error types when using logr.Error": { + text: "test", + err: errors.New("whoops"), + expectedOutput: `ERROR test err="whoops" +`, + }, + } + for n, test := range tests { + t.Run(n, func(t *testing.T) { + var buffer logToBuf + klogr := NewLogger(&buffer, NewConfig()) + for _, name := range test.names { + klogr = klogr.WithName(name) + } + klogr = klogr.WithValues(test.withValues...) + + if test.err != nil { + klogr.Error(test.err, test.text, test.keysAndValues...) + } else { + klogr.Info(test.text, test.keysAndValues...) + } + + actual := buffer.String() + if actual != test.expectedOutput { + t.Errorf("expected %q did not match actual %q", test.expectedOutput, actual) + } + }) + } +} + +func TestCallDepth(t *testing.T) { + logger := NewLogger(t, NewConfig()) + logger.Info("hello world") +} + +type logToBuf struct { + bytes.Buffer +} + +func (l *logToBuf) Helper() { +} + +func (l *logToBuf) Log(args ...interface{}) { + for i, arg := range args { + if i > 0 { + l.Write([]byte(" ")) + } + l.Write([]byte(fmt.Sprintf("%s", arg))) + } + l.Write([]byte("\n")) +} From 64be5637edad883da0b6bca0af865711439095be Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 7 Feb 2022 11:10:05 +0100 Subject: [PATCH 048/125] move Kubernetes helper code into separate file The goal is to have only "legacy" code in klog.go. --- k8s_references.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++ klog.go | 71 ----------------------------------- 2 files changed, 94 insertions(+), 71 deletions(-) create mode 100644 k8s_references.go diff --git a/k8s_references.go b/k8s_references.go new file mode 100644 index 000000000..db58f8baa --- /dev/null +++ b/k8s_references.go @@ -0,0 +1,94 @@ +/* +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 klog + +import ( + "fmt" + "reflect" + + "github.com/go-logr/logr" +) + +// ObjectRef references a kubernetes object +type ObjectRef struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +func (ref ObjectRef) String() string { + if ref.Namespace != "" { + return fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) + } + 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 +type KMetadata interface { + GetName() string + GetNamespace() string +} + +// KObj returns ObjectRef from ObjectMeta +func KObj(obj KMetadata) ObjectRef { + if obj == nil { + return ObjectRef{} + } + if val := reflect.ValueOf(obj); val.Kind() == reflect.Ptr && val.IsNil() { + return ObjectRef{} + } + + return ObjectRef{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } +} + +// KRef returns ObjectRef from name and namespace +func KRef(namespace, name string) ObjectRef { + return ObjectRef{ + Name: name, + 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.go b/klog.go index 3435461d8..d455c45ce 100644 --- a/klog.go +++ b/klog.go @@ -81,7 +81,6 @@ import ( "math" "os" "path/filepath" - "reflect" "runtime" "strconv" "strings" @@ -1536,73 +1535,3 @@ type LogFilter interface { func SetLogFilter(filter LogFilter) { logging.filter = filter } - -// ObjectRef references a kubernetes object -type ObjectRef struct { - Name string `json:"name"` - Namespace string `json:"namespace,omitempty"` -} - -func (ref ObjectRef) String() string { - if ref.Namespace != "" { - return fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) - } - 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 -type KMetadata interface { - GetName() string - GetNamespace() string -} - -// KObj returns ObjectRef from ObjectMeta -func KObj(obj KMetadata) ObjectRef { - if obj == nil { - return ObjectRef{} - } - if val := reflect.ValueOf(obj); val.Kind() == reflect.Ptr && val.IsNil() { - return ObjectRef{} - } - - return ObjectRef{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } -} - -// KRef returns ObjectRef from name and namespace -func KRef(namespace, name string) ObjectRef { - return ObjectRef{ - Name: name, - 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 -} From d8885505366606dbacabd9c781a7aba7ff458e12 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 7 Feb 2022 11:43:16 +0100 Subject: [PATCH 049/125] make klog sufficient for working with logr As discussed in https://github.com/kubernetes/enhancements/pull/3078, enabling code that only imports klog for all logging makes life simpler for developers. This was also the reason why KObj was added to klog instead of a separate helper package. --- imports.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 imports.go diff --git a/imports.go b/imports.go new file mode 100644 index 000000000..43cd08190 --- /dev/null +++ b/imports.go @@ -0,0 +1,58 @@ +/* +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 klog + +import ( + "github.com/go-logr/logr" +) + +// The reason for providing these aliases is to allow code to work with logr +// without directly importing it. + +// Logger in this package is exactly the same as logr.Logger. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type Logger = logr.Logger + +// LogSink in this package is exactly the same as logr.LogSink. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type LogSink = logr.LogSink + +// Runtimeinfo in this package is exactly the same as logr.RuntimeInfo. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type RuntimeInfo = logr.RuntimeInfo + +var ( + // New is an alias for logr.New. + // + // Experimental + // + // Notice: This variable is EXPERIMENTAL and may be changed or removed in a + // later release. + New = logr.New +) From 260ac219dda6b7427653d082b1265b95303166ec Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 7 Feb 2022 12:26:00 +0100 Subject: [PATCH 050/125] contextual logging This moves the global logger into a separate file together with the functions that implement contextual logging. The advantage of SetLoggerWithOptions over alternatives like a SetContextualLogger without options is that it can be used as a drop-in replacement for SetLogger. This allows us to deprecate SetLogger. Whether we actually do that is kept open for now. --- contextual.go | 246 ++++++++++++++++++++++++++++ contextual_test.go | 43 +++++ examples/output_test/output_test.go | 55 ++++++- klog.go | 109 +++++------- klogr.go | 92 +++++++++++ 5 files changed, 473 insertions(+), 72 deletions(-) create mode 100644 contextual.go create mode 100644 contextual_test.go create mode 100644 klogr.go diff --git a/contextual.go b/contextual.go new file mode 100644 index 000000000..67492670d --- /dev/null +++ b/contextual.go @@ -0,0 +1,246 @@ +/* +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 klog + +import ( + "context" + + "github.com/go-logr/logr" +) + +// This file provides the implementation of +// https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/1602-structured-logging +// +// SetLogger and ClearLogger were originally added to klog.go and got moved +// here. Contextual logging adds a way to retrieve a Logger for direct logging +// without the logging calls in klog.go. +// +// The global variables are expected to be modified only during sequential +// parts of a program (init, serial tests) and therefore are not protected by +// mutex locking. + +var ( + // contextualLoggingEnabled controls whether contextual logging is + // active. Disabling it may have some small performance benefit. + contextualLoggingEnabled = true + + // globalLogger is the global Logger chosen by users of klog, nil if + // none is available. + globalLogger *Logger + + // contextualLogger defines whether globalLogger may get called + // directly. + contextualLogger bool + + // klogLogger is used as fallback for logging through the normal klog code + // when no Logger is set. + klogLogger logr.Logger = logr.New(&klogger{}) +) + +// SetLogger sets a Logger implementation that will be used as backing +// implementation of the traditional klog log calls. klog will do its own +// verbosity checks before calling logger.V().Info. logger.Error is always +// called, regardless of the klog verbosity settings. +// +// If set, all log lines will be suppressed from the regular output, and +// redirected to the logr implementation. +// 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. +// +// Modifying the logger is not thread-safe and should be done while no other +// goroutines invoke log calls, usually during program initialization. +func SetLogger(logger logr.Logger) { + SetLoggerWithOptions(logger) +} + +// SetLoggerWithOptions is a more flexible version of SetLogger. Without +// additional options, it behaves exactly like SetLogger. By passing +// ContextualLogger(true) as option, it can be used to set a logger that then +// will also get called directly by applications which retrieve it via +// FromContext, Background, or TODO. +// +// Supporting direct calls is recommended because it avoids the overhead of +// routing log entries through klogr into klog and then into the actual Logger +// backend. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { + globalLogger = &logger + var o loggerOptions + for _, opt := range opts { + opt(&o) + } + contextualLogger = o.contextualLogger +} + +// ContextualLogger determines whether the logger passed to +// SetLoggerWithOptions may also get called directly. Such a logger cannot rely +// on verbosity checking in klog. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func ContextualLogger(enabled bool) LoggerOption { + return func(o *loggerOptions) { + o.contextualLogger = enabled + } +} + +// LoggerOption implements the functional parameter paradigm for +// SetLoggerWithOptions. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type LoggerOption func(o *loggerOptions) + +type loggerOptions struct { + contextualLogger bool +} + +// SetContextualLogger does the same as SetLogger, but in addition the +// logger may also get called directly by code that retrieves it +// with FromContext, TODO or Background. The logger therefore must +// implements its own verbosity checking. +func SetContextualLogger(logger logr.Logger) { + globalLogger = &logger + contextualLogger = true +} + +// ClearLogger removes a backing Logger 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() { + globalLogger = nil +} + +// EnableContextualLogging controls whether contextual logging is enabled. +// By default it is enabled. When disabled, FromContext avoids looking up +// the logger in the context and always returns the global logger. +// LoggerWithValues, LoggerWithName, and NewContext become no-ops +// and return their input logger respectively context. This may be useful +// to avoid the additional overhead for contextual logging. +// +// This must be called during initialization before goroutines are started. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func EnableContextualLogging(enabled bool) { + contextualLoggingEnabled = enabled +} + +// FromContext retrieves a logger set by the caller or, if not set, +// falls back to the program's global logger (a Logger instance or klog +// itself). +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func FromContext(ctx context.Context) Logger { + if contextualLoggingEnabled { + if logger, err := logr.FromContext(ctx); err == nil { + return logger + } + } + + return Background() +} + +// TODO can be used as a last resort by code that has no means of +// receiving a logger from its caller. FromContext or an explicit logger +// parameter should be used instead. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func TODO() Logger { + return Background() +} + +// Background retrieves the fallback logger. It should not be called before +// that logger was initialized by the program and not by code that should +// better receive a logger via its parameters. TODO can be used as a temporary +// solution for such code. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func Background() Logger { + if globalLogger != nil && contextualLogger { + return *globalLogger + } + + return klogLogger +} + +// LoggerWithValues returns logger.WithValues(...kv) when +// contextual logging is enabled, otherwise the logger. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func LoggerWithValues(logger Logger, kv ...interface{}) Logger { + if contextualLoggingEnabled { + return logger.WithValues(kv...) + } + return logger +} + +// LoggerWithName returns logger.WithName(name) when contextual logging is +// enabled, otherwise the logger. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func LoggerWithName(logger Logger, name string) Logger { + if contextualLoggingEnabled { + return logger.WithName(name) + } + return logger +} + +// NewContext returns logr.NewContext(ctx, logger) when +// contextual logging is enabled, otherwise ctx. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func NewContext(ctx context.Context, logger Logger) context.Context { + if contextualLoggingEnabled { + return logr.NewContext(ctx, logger) + } + return ctx +} diff --git a/contextual_test.go b/contextual_test.go new file mode 100644 index 000000000..fc95f05f8 --- /dev/null +++ b/contextual_test.go @@ -0,0 +1,43 @@ +/* +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 klog_test + +import ( + "fmt" + + "github.com/go-logr/logr" + "k8s.io/klog/v2" +) + +func ExampleSetLogger() { + // Logger is only used as backend, Background() returns klogr. + klog.SetLogger(logr.Discard()) + fmt.Printf("logger after SetLogger: %T\n", klog.Background().GetSink()) + + // Logger is only used as backend, Background() returns klogr. + klog.SetLoggerWithOptions(logr.Discard(), klog.ContextualLogger(false)) + fmt.Printf("logger after SetLoggerWithOptions with ContextualLogger(false): %T\n", klog.Background().GetSink()) + + // Logger is used as backend and directly. + klog.SetLoggerWithOptions(logr.Discard(), klog.ContextualLogger(true)) + fmt.Printf("logger after SetLoggerWithOptions with ContextualLogger(true): %T\n", klog.Background().GetSink()) + + // Output: + // logger after SetLogger: *klog.klogger + // logger after SetLoggerWithOptions with ContextualLogger(false): *klog.klogger + // logger after SetLoggerWithOptions with ContextualLogger(true): logr.discardLogSink +} diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index 0624e3a74..b41701217 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -85,7 +85,7 @@ func TestKlogrOutput(t *testing.T) { }) } -// TestKlogrStackText tests klogr -> klog -> text logger. +// TestKlogrStackText tests klogr.klogr -> klog -> text logger. func TestKlogrStackText(t *testing.T) { newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { // Backend: text output. @@ -104,7 +104,7 @@ func TestKlogrStackText(t *testing.T) { test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true}) } -// TestKlogrStackKlogr tests klogr -> klog -> zapr. +// TestKlogrStackKlogr tests klogr.klogr -> klog -> zapr. // // This exposes whether verbosity is passed through correctly // (https://github.com/kubernetes/klog/issues/294) because klogr logging @@ -136,6 +136,57 @@ func TestKlogrStackZapr(t *testing.T) { test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: mapping}) } +// TestKlogrInternalStackText tests klog.klogr (the simplified version used for contextual logging) -> klog -> text logger. +func TestKlogrInternalStackText(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: internal klogr. + return klog.NewKlogr() + } + test.Output(t, test.OutputConfig{NewLogger: newLogger, SupportsVModule: true}) +} + +// TestKlogrInternalStackKlogr tests klog.klogr (the simplified version used for contextual logging) -> klog -> zapr. +// +// This exposes whether verbosity is passed through correctly +// (https://github.com/kubernetes/klog/issues/294) because klogr logging +// records that. +func TestKlogrInternalStackZapr(t *testing.T) { + mapping := test.ZaprOutputMappingIndirect() + + // 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: internal klogr. + return klog.NewKlogr() + } + test.Output(t, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: mapping}) +} + func newZaprLogger(out io.Writer, v int) logr.Logger { encoderConfig := &zapcore.EncoderConfig{ MessageKey: "msg", diff --git a/klog.go b/klog.go index d455c45ce..e2eb0f5c9 100644 --- a/klog.go +++ b/klog.go @@ -483,9 +483,6 @@ type loggingT struct { // If true, add the file directory to the header addDirHeader bool - // If set, all output will be redirected unconditionally to the provided logr.Logger - logr *logr.Logger - // If true, messages will not be propagated to lower severity log levels oneOutput bool @@ -685,7 +682,7 @@ func (l *loggingT) printS(err error, s severity.Severity, depth int, msg string, serialize.KVListFormat(&b.Buffer, "err", err) } serialize.KVListFormat(&b.Buffer, keysAndValues...) - l.printDepth(s, logging.logr, nil, depth+1, &b.Buffer) + l.printDepth(s, globalLogger, nil, depth+1, &b.Buffer) // Make the buffer available for reuse. l.bufferCache.PutBuffer(b) } @@ -707,34 +704,6 @@ func (rb *redirectBuffer) Write(bytes []byte) (n int, err error) { return rb.w.Write(bytes) } -// SetLogger will set the backing logr implementation for klog. -// If set, all log lines will be suppressed from the regular Output, and -// redirected to the logr implementation. -// 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. -// -// 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.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() - - logging.logr = nil -} - // SetOutput sets the output destination for all severities func SetOutput(w io.Writer) { logging.mu.Lock() @@ -790,7 +759,7 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf // TODO: set 'severity' and caller information as structured log info // keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line} if s == severity.ErrorLog { - logging.logr.WithCallDepth(depth+3).Error(nil, string(data)) + globalLogger.WithCallDepth(depth+3).Error(nil, string(data)) } else { log.WithCallDepth(depth + 3).Info(string(data)) } @@ -1111,7 +1080,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.Severity(lb), logging.logr, logging.filter, file, line, true, text) + logging.printWithFileLine(severity.Severity(lb), globalLogger, logging.filter, file, line, true, text) return len(b), nil } @@ -1149,10 +1118,10 @@ type Verbose struct { } func newVerbose(level Level, b bool) Verbose { - if logging.logr == nil { + if globalLogger == nil { return Verbose{b, nil} } - v := logging.logr.V(int(level)) + v := globalLogger.V(int(level)) return Verbose{b, &v} } @@ -1271,7 +1240,7 @@ func (v Verbose) InfoS(msg string, keysAndValues ...interface{}) { // InfoSDepth acts as InfoS but uses depth to determine which call frame to log. // InfoSDepth(0, "msg") is the same as InfoS("msg"). func InfoSDepth(depth int, msg string, keysAndValues ...interface{}) { - logging.infoS(logging.logr, logging.filter, depth, msg, keysAndValues...) + logging.infoS(globalLogger, logging.filter, depth, msg, keysAndValues...) } // InfoSDepth is equivalent to the global InfoSDepth function, guarded by the value of v. @@ -1300,37 +1269,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(severity.InfoLog, logging.logr, logging.filter, args...) + logging.print(severity.InfoLog, globalLogger, 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(severity.InfoLog, logging.logr, logging.filter, depth, args...) + logging.printDepth(severity.InfoLog, globalLogger, 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(severity.InfoLog, logging.logr, logging.filter, args...) + logging.println(severity.InfoLog, globalLogger, 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(severity.InfoLog, logging.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.InfoLog, globalLogger, 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(severity.InfoLog, logging.logr, logging.filter, format, args...) + logging.printf(severity.InfoLog, globalLogger, 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(severity.InfoLog, logging.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.InfoLog, globalLogger, logging.filter, depth, format, args...) } // InfoS structured logs to the INFO log. @@ -1342,79 +1311,79 @@ func InfofDepth(depth int, format string, args ...interface{}) { // output: // >> I1025 00:15:15.525108 1 controller_utils.go:116] "Pod status updated" pod="kubedns" status="ready" func InfoS(msg string, keysAndValues ...interface{}) { - logging.infoS(logging.logr, logging.filter, 0, msg, keysAndValues...) + logging.infoS(globalLogger, logging.filter, 0, msg, keysAndValues...) } // Warning logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Warning(args ...interface{}) { - logging.print(severity.WarningLog, logging.logr, logging.filter, args...) + logging.print(severity.WarningLog, globalLogger, 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(severity.WarningLog, logging.logr, logging.filter, depth, args...) + logging.printDepth(severity.WarningLog, globalLogger, 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(severity.WarningLog, logging.logr, logging.filter, args...) + logging.println(severity.WarningLog, globalLogger, 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(severity.WarningLog, logging.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.WarningLog, globalLogger, 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(severity.WarningLog, logging.logr, logging.filter, format, args...) + logging.printf(severity.WarningLog, globalLogger, 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(severity.WarningLog, logging.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.WarningLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, args...) + logging.print(severity.ErrorLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, depth, args...) + logging.printDepth(severity.ErrorLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, args...) + logging.println(severity.ErrorLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.ErrorLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, format, args...) + logging.printf(severity.ErrorLog, globalLogger, 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(severity.ErrorLog, logging.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.ErrorLog, globalLogger, logging.filter, depth, format, args...) } // ErrorS structured logs to the ERROR, WARNING, and INFO logs. @@ -1427,52 +1396,52 @@ func ErrorfDepth(depth int, format string, args ...interface{}) { // output: // >> E1025 00:15:15.525108 1 controller_utils.go:114] "Failed to update pod status" err="timeout" func ErrorS(err error, msg string, keysAndValues ...interface{}) { - logging.errorS(err, logging.logr, logging.filter, 0, msg, keysAndValues...) + logging.errorS(err, globalLogger, logging.filter, 0, msg, keysAndValues...) } // ErrorSDepth acts as ErrorS but uses depth to determine which call frame to log. // ErrorSDepth(0, "msg") is the same as ErrorS("msg"). func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) { - logging.errorS(err, logging.logr, logging.filter, depth, msg, keysAndValues...) + logging.errorS(err, globalLogger, logging.filter, depth, msg, keysAndValues...) } // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, // including a stack trace of all running goroutines, then calls os.Exit(255). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Fatal(args ...interface{}) { - logging.print(severity.FatalLog, logging.logr, logging.filter, args...) + logging.print(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, args...) + logging.printDepth(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, args...) + logging.println(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, format, args...) + logging.printf(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.FatalLog, globalLogger, logging.filter, depth, format, args...) } // fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks. @@ -1483,41 +1452,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(severity.FatalLog, logging.logr, logging.filter, args...) + logging.print(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, args...) + logging.printDepth(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, args...) + logging.println(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, format, args...) + logging.printf(severity.FatalLog, globalLogger, 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(severity.FatalLog, logging.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.FatalLog, globalLogger, logging.filter, depth, format, args...) } // LogFilter is a collection of functions that can filter all logging calls, diff --git a/klogr.go b/klogr.go new file mode 100644 index 000000000..cdb3834fa --- /dev/null +++ b/klogr.go @@ -0,0 +1,92 @@ +/* +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 klog + +import ( + "github.com/go-logr/logr" + + "k8s.io/klog/v2/internal/serialize" +) + +// NewKlogr returns a logger that is functionally identical to +// klogr.NewWithOptions(klogr.FormatKlog), i.e. it passes through to klog. The +// difference is that it uses a simpler implementation. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func NewKlogr() Logger { + return New(&klogger{}) +} + +// klogger is a subset of klogr/klogr.go. It had to be copied to break an +// import cycle (klogr wants to use klog, and klog wants to use klogr). +type klogger struct { + level int + callDepth int + prefix string + values []interface{} +} + +func (l *klogger) Init(info logr.RuntimeInfo) { + l.callDepth += info.CallDepth +} + +func (l klogger) Info(level int, msg string, kvList ...interface{}) { + trimmed := serialize.TrimDuplicates(l.values, kvList) + if l.prefix != "" { + msg = l.prefix + ": " + msg + } + V(Level(level)).InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...) +} + +func (l klogger) Enabled(level int) bool { + return V(Level(level)).Enabled() +} + +func (l klogger) Error(err error, msg string, kvList ...interface{}) { + trimmed := serialize.TrimDuplicates(l.values, kvList) + if l.prefix != "" { + msg = l.prefix + ": " + msg + } + ErrorSDepth(l.callDepth+1, err, msg, append(trimmed[0], trimmed[1]...)...) +} + +// 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.LogSink { + if len(l.prefix) > 0 { + l.prefix = l.prefix + "/" + } + l.prefix += name + return &l +} + +func (l klogger) WithValues(kvList ...interface{}) logr.LogSink { + l.values = serialize.WithValues(l.values, kvList) + return &l +} + +func (l klogger) WithCallDepth(depth int) logr.LogSink { + l.callDepth += depth + return &l +} + +var _ logr.LogSink = &klogger{} +var _ logr.CallDepthLogSink = &klogger{} From 56b8a4ea35ba0fc3be9fc82182e71c2f0116adc1 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 11 Mar 2022 17:35:28 +0100 Subject: [PATCH 051/125] structured logging: replacing Fatal/Exit/etc. without loss of flushing When structured logging called for functions like Fatal and Exit to be avoided in favor of klog.ErrorS + os.Exit, it was overlooked that this replacement skips flushing. We could ask developers to use klog.ErrorS + klog.Flush + os.Exit, but that is still not quite the same because klog.Flush could get stuck or take too long. Therefore klog now supports flushing + exiting with a dedicated function that can be combined with some other, arbitrary logging function. Allowing applications to change the default timeout and how specifically the application terminates may be useful, so both values are variables that are exported and users of klog are allowed to change. --- exit.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ exit_test.go | 47 +++++++++++++++++++++++++++++++++++ klog.go | 39 +++++++++-------------------- klog_test.go | 3 +++ 4 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 exit.go create mode 100644 exit_test.go diff --git a/exit.go b/exit.go new file mode 100644 index 000000000..320a14772 --- /dev/null +++ b/exit.go @@ -0,0 +1,69 @@ +// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// +// 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 klog + +import ( + "fmt" + "os" + "time" +) + +var ( + + // ExitFlushTimeout is the timeout that klog has traditionally used during + // calls like Fatal or Exit when flushing log data right before exiting. + // Applications that replace those calls and do not have some specific + // requirements like "exit immediately" can use this value as parameter + // for FlushAndExit. + // + // Can be set for testing purpose or to change the application's + // default. + ExitFlushTimeout = 10 * time.Second + + // OsExit is the function called by FlushAndExit to terminate the program. + // + // Can be set for testing purpose or to change the application's + // default behavior. Note that the function should not simply return + // because callers of functions like Fatal will not expect that. + OsExit = os.Exit +) + +// FlushAndExit flushes log data for a certain amount of time and then calls +// os.Exit. Combined with some logging call it provides a replacement for +// traditional calls like Fatal or Exit. +func FlushAndExit(flushTimeout time.Duration, exitCode int) { + timeoutFlush(flushTimeout) + OsExit(exitCode) +} + +// timeoutFlush calls Flush and returns when it completes or after timeout +// elapses, whichever happens first. This is needed because the hooks invoked +// by Flush may deadlock when klog.Fatal is called from a hook that holds +// a lock. Flushing also might take too long. +func timeoutFlush(timeout time.Duration) { + done := make(chan bool, 1) + go func() { + Flush() // calls logging.lockAndFlushAll() + done <- true + }() + select { + case <-done: + case <-time.After(timeout): + fmt.Fprintln(os.Stderr, "klog: Flush took longer than", timeout) + } +} diff --git a/exit_test.go b/exit_test.go new file mode 100644 index 000000000..d0123d971 --- /dev/null +++ b/exit_test.go @@ -0,0 +1,47 @@ +// 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 klog_test + +import ( + "flag" + "fmt" + "os" + + "k8s.io/klog/v2" +) + +func ExampleFlushAndExit() { + // Set up klog so that we can test it below. + var fs flag.FlagSet + klog.InitFlags(&fs) + fs.Set("skip_headers", "true") + defer flag.Set("skip_headers", "false") + klog.SetOutput(os.Stdout) + defer klog.SetOutput(nil) + klog.OsExit = func(exitCode int) { + fmt.Printf("os.Exit(%d)\n", exitCode) + } + + // If we were to return or exit without flushing, this message would + // get lost because it is buffered in memory by klog when writing to + // files. Output to stderr is not buffered. + klog.InfoS("exiting...") + exitCode := 10 + klog.FlushAndExit(klog.ExitFlushTimeout, exitCode) + + // Output: + // "exiting..." + // os.Exit(10) +} diff --git a/klog.go b/klog.go index 0b235f42f..95990fd99 100644 --- a/klog.go +++ b/klog.go @@ -847,8 +847,8 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf if atomic.LoadUint32(&fatalNoStacks) > 0 { l.mu.Unlock() isLocked = false - timeoutFlush(10 * time.Second) - os.Exit(1) + timeoutFlush(ExitFlushTimeout) + OsExit(1) } // Dump all goroutine stacks before exiting. trace := stacks(true) @@ -865,8 +865,8 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf } 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. + timeoutFlush(ExitFlushTimeout) + OsExit(255) // C++ uses -1, which is silly because it's anded with 255 anyway. } l.bufferCache.PutBuffer(buf) @@ -876,23 +876,6 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf } } -// timeoutFlush calls Flush and returns when it completes or after timeout -// elapses, whichever happens first. This is needed because the hooks invoked -// by Flush may deadlock when klog.Fatal is called from a hook that holds -// a lock. -func timeoutFlush(timeout time.Duration) { - done := make(chan bool, 1) - go func() { - Flush() // calls logging.lockAndFlushAll() - done <- true - }() - select { - case <-done: - case <-time.After(timeout): - fmt.Fprintln(os.Stderr, "klog: Flush took longer than", timeout) - } -} - // stacks is a wrapper for runtime.Stack that attempts to recover the data for all goroutines. func stacks(all bool) []byte { // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. @@ -929,7 +912,7 @@ func (l *loggingT) exit(err error) { return } l.flushAll() - os.Exit(2) + OsExit(2) } // syncBuffer joins a bufio.Writer to its underlying file, providing access to the @@ -1518,7 +1501,7 @@ func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) } // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Fatal(args ...interface{}) { logging.print(severity.FatalLog, logging.logr, logging.filter, args...) @@ -1531,7 +1514,7 @@ func FatalDepth(depth int, args ...interface{}) { } // Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Println; a newline is always appended. func Fatalln(args ...interface{}) { logging.println(severity.FatalLog, logging.logr, logging.filter, args...) @@ -1544,7 +1527,7 @@ func FatallnDepth(depth int, args ...interface{}) { } // Fatalf logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). +// including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. func Fatalf(format string, args ...interface{}) { logging.printf(severity.FatalLog, logging.logr, logging.filter, format, args...) @@ -1560,7 +1543,7 @@ func FatalfDepth(depth int, format string, args ...interface{}) { // It allows Exit and relatives to use the Fatal logs. var fatalNoStacks uint32 -// Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). +// Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls OsExit(1). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Exit(args ...interface{}) { atomic.StoreUint32(&fatalNoStacks, 1) @@ -1574,7 +1557,7 @@ func ExitDepth(depth int, args ...interface{}) { 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). +// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls OsExit(1). func Exitln(args ...interface{}) { atomic.StoreUint32(&fatalNoStacks, 1) logging.println(severity.FatalLog, logging.logr, logging.filter, args...) @@ -1587,7 +1570,7 @@ func ExitlnDepth(depth int, args ...interface{}) { 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). +// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls OsExit(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) diff --git a/klog_test.go b/klog_test.go index 95553d5f4..ea9936f14 100644 --- a/klog_test.go +++ b/klog_test.go @@ -1506,6 +1506,7 @@ func TestCallDepthLogrInfoS(t *testing.T) { logger.resetCallDepth() l := logr.New(logger) SetLogger(l) + defer ClearLogger() // Add wrapper to ensure callDepthTestLogr +2 offset is correct. logFunc := func() { @@ -1528,6 +1529,7 @@ func TestCallDepthLogrErrorS(t *testing.T) { logger.resetCallDepth() l := logr.New(logger) SetLogger(l) + defer ClearLogger() // Add wrapper to ensure callDepthTestLogr +2 offset is correct. logFunc := func() { @@ -1550,6 +1552,7 @@ func TestCallDepthLogrGoLog(t *testing.T) { logger.resetCallDepth() l := logr.New(logger) SetLogger(l) + defer ClearLogger() CopyStandardLogTo("INFO") // Add wrapper to ensure callDepthTestLogr +2 offset is correct. From 0013f225ca16fd06d5876420a2e33f4df7f0ff97 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 15 Mar 2022 14:17:45 +0100 Subject: [PATCH 052/125] exit_test.go: avoid depending on side effect of other tests Logging to file must be enabled explicitly. This happened to work randomly before, depending on which other tests were run. --- exit_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exit_test.go b/exit_test.go index d0123d971..e295eb8e8 100644 --- a/exit_test.go +++ b/exit_test.go @@ -24,10 +24,13 @@ import ( func ExampleFlushAndExit() { // Set up klog so that we can test it below. + var fs flag.FlagSet klog.InitFlags(&fs) fs.Set("skip_headers", "true") defer flag.Set("skip_headers", "false") + fs.Set("logtostderr", "false") + defer fs.Set("logtostderr", "true") klog.SetOutput(os.Stdout) defer klog.SetOutput(nil) klog.OsExit = func(exitCode int) { From cd54fdc3e0faffd626dfa7c182837ebec9f0da0e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 15 Mar 2022 14:24:41 +0100 Subject: [PATCH 053/125] ExampleSetLogger: reset state after test When running as a test, the global state must be restored because other tests also get run. --- contextual_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contextual_test.go b/contextual_test.go index fc95f05f8..cb588676a 100644 --- a/contextual_test.go +++ b/contextual_test.go @@ -24,6 +24,8 @@ import ( ) func ExampleSetLogger() { + defer klog.ClearLogger() + // Logger is only used as backend, Background() returns klogr. klog.SetLogger(logr.Discard()) fmt.Printf("logger after SetLogger: %T\n", klog.Background().GetSink()) From 1e923c616a2c74ef58de840f0cf7b966e0a184d2 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Feb 2022 14:13:19 +0100 Subject: [PATCH 054/125] tests: stop testing with Go 1.14 Go 1.14.x is no longer supported upstream and doesn't have some newer functionality like testing.T.TempDir. Core klog may still work with Go < 1.15, it just doesn't get tested anymore. --- .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 38793a372..841ee58c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.14, 1.15, 1.16, 1.17] + go-version: [1.15, 1.16, 1.17] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From f5927b06bf737f44efc5dc3a1904e3ed7335349e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Mar 2022 12:05:53 +0100 Subject: [PATCH 055/125] SetLoggerWithOptions: support flushing When setting a logger that buffers data in memory, we want klog.Flush to flush that data. For that the caller has to provide an additional callback because logr.Logger has no API for flushing. --- contextual.go | 27 +++++++++++++++++++++++---- contextual_test.go | 13 +++++++++++++ klog.go | 3 +++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/contextual.go b/contextual.go index 67492670d..bb0380896 100644 --- a/contextual.go +++ b/contextual.go @@ -42,6 +42,10 @@ var ( // none is available. globalLogger *Logger + // globalLoggerOptions contains the options that were supplied for + // globalLogger. + globalLoggerOptions loggerOptions + // contextualLogger defines whether globalLogger may get called // directly. contextualLogger bool @@ -87,11 +91,10 @@ func SetLogger(logger logr.Logger) { // later release. func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { globalLogger = &logger - var o loggerOptions + globalLoggerOptions = loggerOptions{} for _, opt := range opts { - opt(&o) + opt(&globalLoggerOptions) } - contextualLogger = o.contextualLogger } // ContextualLogger determines whether the logger passed to @@ -108,6 +111,18 @@ func ContextualLogger(enabled bool) LoggerOption { } } +// FlushLogger provides a callback for flushing data buffered by the logger. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func FlushLogger(flush func()) LoggerOption { + return func(o *loggerOptions) { + o.flush = flush + } +} + // LoggerOption implements the functional parameter paradigm for // SetLoggerWithOptions. // @@ -119,6 +134,7 @@ type LoggerOption func(o *loggerOptions) type loggerOptions struct { contextualLogger bool + flush func() } // SetContextualLogger does the same as SetLogger, but in addition the @@ -137,6 +153,7 @@ func SetContextualLogger(logger logr.Logger) { // goroutines invoke log calls, usually during program initialization. func ClearLogger() { globalLogger = nil + globalLoggerOptions = loggerOptions{} } // EnableContextualLogging controls whether contextual logging is enabled. @@ -196,7 +213,9 @@ func TODO() Logger { // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. func Background() Logger { - if globalLogger != nil && contextualLogger { + if globalLoggerOptions.contextualLogger { + // Is non-nil because globalLoggerOptions.contextualLogger is + // only true if a logger was set. return *globalLogger } diff --git a/contextual_test.go b/contextual_test.go index cb588676a..80e14fa30 100644 --- a/contextual_test.go +++ b/contextual_test.go @@ -43,3 +43,16 @@ func ExampleSetLogger() { // logger after SetLoggerWithOptions with ContextualLogger(false): *klog.klogger // logger after SetLoggerWithOptions with ContextualLogger(true): logr.discardLogSink } + +func ExampleFlushLogger() { + defer klog.ClearLogger() + + // This simple logger doesn't need flushing, but others might. + klog.SetLoggerWithOptions(logr.Discard(), klog.FlushLogger(func() { + fmt.Print("flushing...") + })) + klog.Flush() + + // Output: + // flushing... +} diff --git a/klog.go b/klog.go index 7eae51924..15710be3f 100644 --- a/klog.go +++ b/klog.go @@ -1097,6 +1097,9 @@ func (l *loggingT) flushAll() { file.Sync() // ignore error } } + if globalLoggerOptions.flush != nil { + globalLoggerOptions.flush() + } } // CopyStandardLogTo arranges for messages written to the Go "log" package's From 6684fc77d7b4430b0b152e4293b12ea70eb31800 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Mar 2022 17:10:17 +0100 Subject: [PATCH 056/125] StartFlushDaemon: add API with more control over flushing Previously, periodic flushing was started automatically when writing to buffered files for the first time. When writing to a Logger which buffers internally, then using the same periodic flushing makes sense. In that case it has to be started explicitly. The new call grants the caller control over the flush interval. Kubernetes has a parameter for that. --- klog.go | 30 +++++++++++++++++++++--------- klog_test.go | 19 ++++++++++--------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/klog.go b/klog.go index 15710be3f..bb6f64be4 100644 --- a/klog.go +++ b/klog.go @@ -395,7 +395,7 @@ func init() { logging.addDirHeader = false logging.skipLogHeaders = false logging.oneOutput = false - logging.flushD = newFlushDaemon(flushInterval, logging.lockAndFlushAll, nil) + logging.flushD = newFlushDaemon(logging.lockAndFlushAll, nil) } // InitFlags is for explicitly initializing the flags. @@ -449,6 +449,9 @@ type loggingT struct { file [severity.NumSeverity]flushSyncWriter // flushD holds a flushDaemon that frequently flushes log file buffers. flushD *flushDaemon + // flushInterval is the interval for periodic flushing. If zero, + // the global default will be used. + flushInterval time.Duration // 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. @@ -976,7 +979,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.Severity) error { - l.flushD.run() + interval := l.flushInterval + if interval == 0 { + interval = flushInterval + } + l.flushD.run(interval) now := time.Now() // Files are created in decreasing severity order, so as soon as we find one // has already been created, we can stop. @@ -1000,7 +1007,6 @@ const flushInterval = 5 * time.Second type flushDaemon struct { mu sync.Mutex clock clock.WithTicker - interval time.Duration flush func() stopC chan struct{} stopDone chan struct{} @@ -1008,20 +1014,19 @@ type flushDaemon struct { // newFlushDaemon returns a new flushDaemon. If the passed clock is nil, a // clock.RealClock is used. -func newFlushDaemon(interval time.Duration, flush func(), tickClock clock.WithTicker) *flushDaemon { +func newFlushDaemon(flush func(), tickClock clock.WithTicker) *flushDaemon { if tickClock == nil { tickClock = clock.RealClock{} } return &flushDaemon{ - interval: interval, - flush: flush, - clock: tickClock, + flush: flush, + clock: tickClock, } } // run starts a goroutine that periodically calls the daemons flush function. // Calling run on an already running daemon will have no effect. -func (f *flushDaemon) run() { +func (f *flushDaemon) run(interval time.Duration) { f.mu.Lock() defer f.mu.Unlock() @@ -1032,7 +1037,7 @@ func (f *flushDaemon) run() { f.stopC = make(chan struct{}, 1) f.stopDone = make(chan struct{}, 1) - ticker := f.clock.NewTicker(f.interval) + ticker := f.clock.NewTicker(interval) go func() { defer ticker.Stop() defer func() { f.stopDone <- struct{}{} }() @@ -1079,6 +1084,13 @@ func StopFlushDaemon() { logging.flushD.stop() } +// StartFlushDaemon ensures that the flush daemon runs with the given delay +// between flush calls. If it is already running, it gets restarted. +func StartFlushDaemon(interval time.Duration) { + StopFlushDaemon() + logging.flushD.run(interval) +} + // lockAndFlushAll is like flushAll but locks l.mu first. func (l *loggingT) lockAndFlushAll() { l.mu.Lock() diff --git a/klog_test.go b/klog_test.go index ea9936f14..0a1657481 100644 --- a/klog_test.go +++ b/klog_test.go @@ -381,8 +381,8 @@ func TestSetOutputDataRace(t *testing.T) { var wg sync.WaitGroup var daemons []*flushDaemon for i := 1; i <= 50; i++ { - daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) - daemon.run() + daemon := newFlushDaemon(logging.lockAndFlushAll, nil) + daemon.run(time.Second) daemons = append(daemons, daemon) } for i := 1; i <= 50; i++ { @@ -393,8 +393,8 @@ func TestSetOutputDataRace(t *testing.T) { }() } for i := 1; i <= 50; i++ { - daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) - daemon.run() + daemon := newFlushDaemon(logging.lockAndFlushAll, nil) + daemon.run(time.Second) daemons = append(daemons, daemon) } for i := 1; i <= 50; i++ { @@ -405,8 +405,8 @@ func TestSetOutputDataRace(t *testing.T) { }() } for i := 1; i <= 50; i++ { - daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil) - daemon.run() + daemon := newFlushDaemon(logging.lockAndFlushAll, nil) + daemon.run(time.Second) daemons = append(daemons, daemon) } wg.Wait() @@ -1869,7 +1869,8 @@ func TestFlushDaemon(t *testing.T) { } testClock := testingclock.NewFakeClock(time.Now()) testLog := loggingT{ - flushD: newFlushDaemon(time.Second, spyFunc, testClock), + flushInterval: time.Second, + flushD: newFlushDaemon(spyFunc, testClock), } // Calling testLog will call createFile, which should start the daemon. @@ -1910,8 +1911,8 @@ func TestFlushDaemon(t *testing.T) { func TestStopFlushDaemon(t *testing.T) { logging.flushD.stop() - logging.flushD = newFlushDaemon(time.Second, func() {}, nil) - logging.flushD.run() + logging.flushD = newFlushDaemon(func() {}, nil) + logging.flushD.run(time.Second) if !logging.flushD.isRunning() { t.Error("expected flushD to be running") } From fb03dab351fe618bb2e4dc6c73cd745251d962c4 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 17 Mar 2022 12:52:53 +0100 Subject: [PATCH 057/125] SetContextualLogger: remove unintentionally merged API call The SetContextualLogger call is from an earlier draft for contextual logging and was replaced by SetLoggerWithOptions. It shouldn't have been merged in the first place. --- contextual.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/contextual.go b/contextual.go index bb0380896..33743ffb8 100644 --- a/contextual.go +++ b/contextual.go @@ -46,10 +46,6 @@ var ( // globalLogger. globalLoggerOptions loggerOptions - // contextualLogger defines whether globalLogger may get called - // directly. - contextualLogger bool - // klogLogger is used as fallback for logging through the normal klog code // when no Logger is set. klogLogger logr.Logger = logr.New(&klogger{}) @@ -137,15 +133,6 @@ type loggerOptions struct { flush func() } -// SetContextualLogger does the same as SetLogger, but in addition the -// logger may also get called directly by code that retrieves it -// with FromContext, TODO or Background. The logger therefore must -// implements its own verbosity checking. -func SetContextualLogger(logger logr.Logger) { - globalLogger = &logger - contextualLogger = true -} - // ClearLogger removes a backing Logger implementation if one was set earlier // with SetLogger. // From 48ee3dda9f799c6f5e5ff33f5d46c38f98e4bcec Mon Sep 17 00:00:00 2001 From: Marek Siarkowicz Date: Thu, 17 Mar 2022 18:11:04 +0100 Subject: [PATCH 058/125] Cleanup OWNERS file maintainers prune --repository-devstats=kubernetes/klog --repository-github=kubernetes/klog --dryrun=false Running script : 03-17-2022 18:10:19 Found 0 unique aliases Found 16 unique users ....... >>>>> Contributions from kubernetes/klog devstats repo and kubernetes/klog github repo : 7 >>>>> GitHub ID : Devstats contrib count : GitHub PR comment count dims : 115 : 44 pohly : 298 : 33 serathius : 85 : 18 thockin : 56 : 8 jayunit100 : 2 : 2 neolit123 : 3 : 2 vincepri : 2 : 0 >>>>> Missing Contributions in kubernetes/klog (devstats == 0): 9 andyxning brancz detiber hoegaarden justinsb lavalamp piosz tallclair yagonobre >>>>> Low reviews/approvals in kubernetes/klog (GH pr comments <= 10 && devstats <=20): 3 jayunit100 neolit123 vincepri --- OWNERS | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/OWNERS b/OWNERS index ad5063fdf..8cccebf2e 100644 --- a/OWNERS +++ b/OWNERS @@ -1,19 +1,13 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: - - jayunit100 - - hoegaarden - - andyxning - - neolit123 - pohly - - yagonobre - - vincepri - - detiber approvers: - dims - thockin - - justinsb - - tallclair - - piosz + - serathius +emeritus_approvers: - brancz + - justinsb - lavalamp - - serathius + - piosz + - tallclair From e3af9af8c72958447814fdae84dffc482f5c2ca3 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 18 Mar 2022 18:05:58 +0100 Subject: [PATCH 059/125] dependencies: avoid k8s.io/utils, fork clock code instead The test for the flush daemon needs the simulated clock package, but we cannot use k8s.io/utils in klog because it causes a circular dependency. We could drop the test, but it is useful, so instead we fork the package and carry a copy in "internal". --- go.mod | 5 +- go.sum | 11 - internal/clock/README.md | 7 + internal/clock/clock.go | 178 +++++++++ internal/clock/testing/fake_clock.go | 361 ++++++++++++++++++ .../clock/testing/simple_interval_clock.go | 44 +++ klog.go | 2 +- klog_test.go | 2 +- 8 files changed, 593 insertions(+), 17 deletions(-) create mode 100644 internal/clock/README.md create mode 100644 internal/clock/clock.go create mode 100644 internal/clock/testing/fake_clock.go create mode 100644 internal/clock/testing/simple_interval_clock.go diff --git a/go.mod b/go.mod index 6385c6cde..31aefba74 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module k8s.io/klog/v2 go 1.13 -require ( - github.com/go-logr/logr v1.2.0 - k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 -) +require github.com/go-logr/logr v1.2.0 diff --git a/go.sum b/go.sum index ef731394a..919fbadbc 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,2 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -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= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/internal/clock/README.md b/internal/clock/README.md new file mode 100644 index 000000000..03d692c8f --- /dev/null +++ b/internal/clock/README.md @@ -0,0 +1,7 @@ +# Clock + +This package provides an interface for time-based operations. It allows +mocking time for testing. + +This is a copy of k8s.io/utils/clock. We have to copy it to avoid a circular +dependency (k8s.io/klog -> k8s.io/utils -> k8s.io/klog). diff --git a/internal/clock/clock.go b/internal/clock/clock.go new file mode 100644 index 000000000..b8b6af5c8 --- /dev/null +++ b/internal/clock/clock.go @@ -0,0 +1,178 @@ +/* +Copyright 2014 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 clock + +import "time" + +// PassiveClock allows for injecting fake or real clocks into code +// that needs to read the current time but does not support scheduling +// activity in the future. +type PassiveClock interface { + Now() time.Time + Since(time.Time) time.Duration +} + +// Clock allows for injecting fake or real clocks into code that +// needs to do arbitrary things based on time. +type Clock interface { + PassiveClock + // After returns the channel of a new Timer. + // This method does not allow to free/GC the backing timer before it fires. Use + // NewTimer instead. + After(d time.Duration) <-chan time.Time + // NewTimer returns a new Timer. + NewTimer(d time.Duration) Timer + // Sleep sleeps for the provided duration d. + // Consider making the sleep interruptible by using 'select' on a context channel and a timer channel. + Sleep(d time.Duration) + // Tick returns the channel of a new Ticker. + // This method does not allow to free/GC the backing ticker. Use + // NewTicker from WithTicker instead. + Tick(d time.Duration) <-chan time.Time +} + +// WithTicker allows for injecting fake or real clocks into code that +// needs to do arbitrary things based on time. +type WithTicker interface { + Clock + // NewTicker returns a new Ticker. + NewTicker(time.Duration) Ticker +} + +// WithDelayedExecution allows for injecting fake or real clocks into +// code that needs to make use of AfterFunc functionality. +type WithDelayedExecution interface { + Clock + // AfterFunc executes f in its own goroutine after waiting + // for d duration and returns a Timer whose channel can be + // closed by calling Stop() on the Timer. + AfterFunc(d time.Duration, f func()) Timer +} + +// WithTickerAndDelayedExecution allows for injecting fake or real clocks +// into code that needs Ticker and AfterFunc functionality +type WithTickerAndDelayedExecution interface { + WithTicker + // AfterFunc executes f in its own goroutine after waiting + // for d duration and returns a Timer whose channel can be + // closed by calling Stop() on the Timer. + AfterFunc(d time.Duration, f func()) Timer +} + +// Ticker defines the Ticker interface. +type Ticker interface { + C() <-chan time.Time + Stop() +} + +var _ = WithTicker(RealClock{}) + +// RealClock really calls time.Now() +type RealClock struct{} + +// Now returns the current time. +func (RealClock) Now() time.Time { + return time.Now() +} + +// Since returns time since the specified timestamp. +func (RealClock) Since(ts time.Time) time.Duration { + return time.Since(ts) +} + +// After is the same as time.After(d). +// This method does not allow to free/GC the backing timer before it fires. Use +// NewTimer instead. +func (RealClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +// NewTimer is the same as time.NewTimer(d) +func (RealClock) NewTimer(d time.Duration) Timer { + return &realTimer{ + timer: time.NewTimer(d), + } +} + +// AfterFunc is the same as time.AfterFunc(d, f). +func (RealClock) AfterFunc(d time.Duration, f func()) Timer { + return &realTimer{ + timer: time.AfterFunc(d, f), + } +} + +// Tick is the same as time.Tick(d) +// This method does not allow to free/GC the backing ticker. Use +// NewTicker instead. +func (RealClock) Tick(d time.Duration) <-chan time.Time { + return time.Tick(d) +} + +// NewTicker returns a new Ticker. +func (RealClock) NewTicker(d time.Duration) Ticker { + return &realTicker{ + ticker: time.NewTicker(d), + } +} + +// Sleep is the same as time.Sleep(d) +// Consider making the sleep interruptible by using 'select' on a context channel and a timer channel. +func (RealClock) Sleep(d time.Duration) { + time.Sleep(d) +} + +// Timer allows for injecting fake or real timers into code that +// needs to do arbitrary things based on time. +type Timer interface { + C() <-chan time.Time + Stop() bool + Reset(d time.Duration) bool +} + +var _ = Timer(&realTimer{}) + +// realTimer is backed by an actual time.Timer. +type realTimer struct { + timer *time.Timer +} + +// C returns the underlying timer's channel. +func (r *realTimer) C() <-chan time.Time { + return r.timer.C +} + +// Stop calls Stop() on the underlying timer. +func (r *realTimer) Stop() bool { + return r.timer.Stop() +} + +// Reset calls Reset() on the underlying timer. +func (r *realTimer) Reset(d time.Duration) bool { + return r.timer.Reset(d) +} + +type realTicker struct { + ticker *time.Ticker +} + +func (r *realTicker) C() <-chan time.Time { + return r.ticker.C +} + +func (r *realTicker) Stop() { + r.ticker.Stop() +} diff --git a/internal/clock/testing/fake_clock.go b/internal/clock/testing/fake_clock.go new file mode 100644 index 000000000..a91114a58 --- /dev/null +++ b/internal/clock/testing/fake_clock.go @@ -0,0 +1,361 @@ +/* +Copyright 2014 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 testing + +import ( + "sync" + "time" + + "k8s.io/klog/v2/internal/clock" +) + +var ( + _ = clock.PassiveClock(&FakePassiveClock{}) + _ = clock.WithTicker(&FakeClock{}) + _ = clock.Clock(&IntervalClock{}) +) + +// FakePassiveClock implements PassiveClock, but returns an arbitrary time. +type FakePassiveClock struct { + lock sync.RWMutex + time time.Time +} + +// FakeClock implements clock.Clock, but returns an arbitrary time. +type FakeClock struct { + FakePassiveClock + + // waiters are waiting for the fake time to pass their specified time + waiters []*fakeClockWaiter +} + +type fakeClockWaiter struct { + targetTime time.Time + stepInterval time.Duration + skipIfBlocked bool + destChan chan time.Time + fired bool + afterFunc func() +} + +// NewFakePassiveClock returns a new FakePassiveClock. +func NewFakePassiveClock(t time.Time) *FakePassiveClock { + return &FakePassiveClock{ + time: t, + } +} + +// NewFakeClock constructs a fake clock set to the provided time. +func NewFakeClock(t time.Time) *FakeClock { + return &FakeClock{ + FakePassiveClock: *NewFakePassiveClock(t), + } +} + +// Now returns f's time. +func (f *FakePassiveClock) Now() time.Time { + f.lock.RLock() + defer f.lock.RUnlock() + return f.time +} + +// Since returns time since the time in f. +func (f *FakePassiveClock) Since(ts time.Time) time.Duration { + f.lock.RLock() + defer f.lock.RUnlock() + return f.time.Sub(ts) +} + +// SetTime sets the time on the FakePassiveClock. +func (f *FakePassiveClock) SetTime(t time.Time) { + f.lock.Lock() + defer f.lock.Unlock() + f.time = t +} + +// After is the fake version of time.After(d). +func (f *FakeClock) After(d time.Duration) <-chan time.Time { + f.lock.Lock() + defer f.lock.Unlock() + stopTime := f.time.Add(d) + ch := make(chan time.Time, 1) // Don't block! + f.waiters = append(f.waiters, &fakeClockWaiter{ + targetTime: stopTime, + destChan: ch, + }) + return ch +} + +// NewTimer constructs a fake timer, akin to time.NewTimer(d). +func (f *FakeClock) NewTimer(d time.Duration) clock.Timer { + f.lock.Lock() + defer f.lock.Unlock() + stopTime := f.time.Add(d) + ch := make(chan time.Time, 1) // Don't block! + timer := &fakeTimer{ + fakeClock: f, + waiter: fakeClockWaiter{ + targetTime: stopTime, + destChan: ch, + }, + } + f.waiters = append(f.waiters, &timer.waiter) + return timer +} + +// AfterFunc is the Fake version of time.AfterFunc(d, cb). +func (f *FakeClock) AfterFunc(d time.Duration, cb func()) clock.Timer { + f.lock.Lock() + defer f.lock.Unlock() + stopTime := f.time.Add(d) + ch := make(chan time.Time, 1) // Don't block! + + timer := &fakeTimer{ + fakeClock: f, + waiter: fakeClockWaiter{ + targetTime: stopTime, + destChan: ch, + afterFunc: cb, + }, + } + f.waiters = append(f.waiters, &timer.waiter) + return timer +} + +// Tick constructs a fake ticker, akin to time.Tick +func (f *FakeClock) Tick(d time.Duration) <-chan time.Time { + if d <= 0 { + return nil + } + f.lock.Lock() + defer f.lock.Unlock() + tickTime := f.time.Add(d) + ch := make(chan time.Time, 1) // hold one tick + f.waiters = append(f.waiters, &fakeClockWaiter{ + targetTime: tickTime, + stepInterval: d, + skipIfBlocked: true, + destChan: ch, + }) + + return ch +} + +// NewTicker returns a new Ticker. +func (f *FakeClock) NewTicker(d time.Duration) clock.Ticker { + f.lock.Lock() + defer f.lock.Unlock() + tickTime := f.time.Add(d) + ch := make(chan time.Time, 1) // hold one tick + f.waiters = append(f.waiters, &fakeClockWaiter{ + targetTime: tickTime, + stepInterval: d, + skipIfBlocked: true, + destChan: ch, + }) + + return &fakeTicker{ + c: ch, + } +} + +// Step moves the clock by Duration and notifies anyone that's called After, +// Tick, or NewTimer. +func (f *FakeClock) Step(d time.Duration) { + f.lock.Lock() + defer f.lock.Unlock() + f.setTimeLocked(f.time.Add(d)) +} + +// SetTime sets the time. +func (f *FakeClock) SetTime(t time.Time) { + f.lock.Lock() + defer f.lock.Unlock() + f.setTimeLocked(t) +} + +// Actually changes the time and checks any waiters. f must be write-locked. +func (f *FakeClock) setTimeLocked(t time.Time) { + f.time = t + newWaiters := make([]*fakeClockWaiter, 0, len(f.waiters)) + for i := range f.waiters { + w := f.waiters[i] + if !w.targetTime.After(t) { + if w.skipIfBlocked { + select { + case w.destChan <- t: + w.fired = true + default: + } + } else { + w.destChan <- t + w.fired = true + } + + if w.afterFunc != nil { + w.afterFunc() + } + + if w.stepInterval > 0 { + for !w.targetTime.After(t) { + w.targetTime = w.targetTime.Add(w.stepInterval) + } + newWaiters = append(newWaiters, w) + } + + } else { + newWaiters = append(newWaiters, f.waiters[i]) + } + } + f.waiters = newWaiters +} + +// HasWaiters returns true if After or AfterFunc has been called on f but not yet satisfied (so you can +// write race-free tests). +func (f *FakeClock) HasWaiters() bool { + f.lock.RLock() + defer f.lock.RUnlock() + return len(f.waiters) > 0 +} + +// Sleep is akin to time.Sleep +func (f *FakeClock) Sleep(d time.Duration) { + f.Step(d) +} + +// IntervalClock implements clock.PassiveClock, but each invocation of Now steps the clock forward the specified duration. +// IntervalClock technically implements the other methods of clock.Clock, but each implementation is just a panic. +// +// Deprecated: See SimpleIntervalClock for an alternative that only has the methods of PassiveClock. +type IntervalClock struct { + Time time.Time + Duration time.Duration +} + +// Now returns i's time. +func (i *IntervalClock) Now() time.Time { + i.Time = i.Time.Add(i.Duration) + return i.Time +} + +// Since returns time since the time in i. +func (i *IntervalClock) Since(ts time.Time) time.Duration { + return i.Time.Sub(ts) +} + +// After is unimplemented, will panic. +// TODO: make interval clock use FakeClock so this can be implemented. +func (*IntervalClock) After(d time.Duration) <-chan time.Time { + panic("IntervalClock doesn't implement After") +} + +// NewTimer is unimplemented, will panic. +// TODO: make interval clock use FakeClock so this can be implemented. +func (*IntervalClock) NewTimer(d time.Duration) clock.Timer { + panic("IntervalClock doesn't implement NewTimer") +} + +// AfterFunc is unimplemented, will panic. +// TODO: make interval clock use FakeClock so this can be implemented. +func (*IntervalClock) AfterFunc(d time.Duration, f func()) clock.Timer { + panic("IntervalClock doesn't implement AfterFunc") +} + +// Tick is unimplemented, will panic. +// TODO: make interval clock use FakeClock so this can be implemented. +func (*IntervalClock) Tick(d time.Duration) <-chan time.Time { + panic("IntervalClock doesn't implement Tick") +} + +// NewTicker has no implementation yet and is omitted. +// TODO: make interval clock use FakeClock so this can be implemented. +func (*IntervalClock) NewTicker(d time.Duration) clock.Ticker { + panic("IntervalClock doesn't implement NewTicker") +} + +// Sleep is unimplemented, will panic. +func (*IntervalClock) Sleep(d time.Duration) { + panic("IntervalClock doesn't implement Sleep") +} + +var _ = clock.Timer(&fakeTimer{}) + +// fakeTimer implements clock.Timer based on a FakeClock. +type fakeTimer struct { + fakeClock *FakeClock + waiter fakeClockWaiter +} + +// C returns the channel that notifies when this timer has fired. +func (f *fakeTimer) C() <-chan time.Time { + return f.waiter.destChan +} + +// Stop stops the timer and returns true if the timer has not yet fired, or false otherwise. +func (f *fakeTimer) Stop() bool { + f.fakeClock.lock.Lock() + defer f.fakeClock.lock.Unlock() + + newWaiters := make([]*fakeClockWaiter, 0, len(f.fakeClock.waiters)) + for i := range f.fakeClock.waiters { + w := f.fakeClock.waiters[i] + if w != &f.waiter { + newWaiters = append(newWaiters, w) + } + } + + f.fakeClock.waiters = newWaiters + + return !f.waiter.fired +} + +// Reset resets the timer to the fake clock's "now" + d. It returns true if the timer has not yet +// fired, or false otherwise. +func (f *fakeTimer) Reset(d time.Duration) bool { + f.fakeClock.lock.Lock() + defer f.fakeClock.lock.Unlock() + + active := !f.waiter.fired + + f.waiter.fired = false + f.waiter.targetTime = f.fakeClock.time.Add(d) + + var isWaiting bool + for i := range f.fakeClock.waiters { + w := f.fakeClock.waiters[i] + if w == &f.waiter { + isWaiting = true + break + } + } + if !isWaiting { + f.fakeClock.waiters = append(f.fakeClock.waiters, &f.waiter) + } + + return active +} + +type fakeTicker struct { + c <-chan time.Time +} + +func (t *fakeTicker) C() <-chan time.Time { + return t.c +} + +func (t *fakeTicker) Stop() { +} diff --git a/internal/clock/testing/simple_interval_clock.go b/internal/clock/testing/simple_interval_clock.go new file mode 100644 index 000000000..be2677360 --- /dev/null +++ b/internal/clock/testing/simple_interval_clock.go @@ -0,0 +1,44 @@ +/* +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 testing + +import ( + "time" + + "k8s.io/klog/v2/internal/clock" +) + +var ( + _ = clock.PassiveClock(&SimpleIntervalClock{}) +) + +// SimpleIntervalClock implements clock.PassiveClock, but each invocation of Now steps the clock forward the specified duration +type SimpleIntervalClock struct { + Time time.Time + Duration time.Duration +} + +// Now returns i's time. +func (i *SimpleIntervalClock) Now() time.Time { + i.Time = i.Time.Add(i.Duration) + return i.Time +} + +// Since returns time since the time in i. +func (i *SimpleIntervalClock) Since(ts time.Time) time.Duration { + return i.Time.Sub(ts) +} diff --git a/klog.go b/klog.go index bb6f64be4..cb04590fe 100644 --- a/klog.go +++ b/klog.go @@ -91,9 +91,9 @@ import ( "github.com/go-logr/logr" "k8s.io/klog/v2/internal/buffer" + "k8s.io/klog/v2/internal/clock" "k8s.io/klog/v2/internal/serialize" "k8s.io/klog/v2/internal/severity" - "k8s.io/utils/clock" ) // severityValue identifies the sort of log: info, warning etc. It also implements diff --git a/klog_test.go b/klog_test.go index 0a1657481..a23f5716f 100644 --- a/klog_test.go +++ b/klog_test.go @@ -37,9 +37,9 @@ import ( "github.com/go-logr/logr" "k8s.io/klog/v2/internal/buffer" + testingclock "k8s.io/klog/v2/internal/clock/testing" "k8s.io/klog/v2/internal/severity" "k8s.io/klog/v2/internal/test" - testingclock "k8s.io/utils/clock/testing" ) // TODO: This test package should be refactored so that tests cannot From 36e44e76855c05d8a66f6aecd501bdcf580fcf0d Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 18 Mar 2022 19:17:31 +0100 Subject: [PATCH 060/125] promote non-test APIs to stable As pointed out in https://github.com/kubernetes/kubernetes/pull/108725#discussion_r830100796, klog cannot carry "experimental" APIs that may end up in code that Kubernetes depends on, whether it is directly or indirectly. If that happens, Kubernetes might get stuck on a certain klog release because one dependency depends on one klog release and another dependency on a different, incompatible one. --- README.md | 7 +++++-- contextual.go | 55 --------------------------------------------------- imports.go | 20 ------------------- klogr.go | 5 ----- 4 files changed, 5 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index a9c945e1d..7de2212cc 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,15 @@ Historical context is available here: 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 +- `k8s.io/hack/tools` - no stable API yet (may change eventually or get moved to separate repo), `hack/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. +may still change in incompatible ways or get removed entirely. This can only +be used for code that is used in tests to avoid situations where non-test +code from two different Kubernetes dependencies depends on incompatible +releases of klog because an experimental API was changed. ---- diff --git a/contextual.go b/contextual.go index bb0380896..17e899ae7 100644 --- a/contextual.go +++ b/contextual.go @@ -84,11 +84,6 @@ func SetLogger(logger logr.Logger) { // Supporting direct calls is recommended because it avoids the overhead of // routing log entries through klogr into klog and then into the actual Logger // backend. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { globalLogger = &logger globalLoggerOptions = loggerOptions{} @@ -100,11 +95,6 @@ func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { // ContextualLogger determines whether the logger passed to // SetLoggerWithOptions may also get called directly. Such a logger cannot rely // on verbosity checking in klog. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func ContextualLogger(enabled bool) LoggerOption { return func(o *loggerOptions) { o.contextualLogger = enabled @@ -112,11 +102,6 @@ func ContextualLogger(enabled bool) LoggerOption { } // FlushLogger provides a callback for flushing data buffered by the logger. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func FlushLogger(flush func()) LoggerOption { return func(o *loggerOptions) { o.flush = flush @@ -125,11 +110,6 @@ func FlushLogger(flush func()) LoggerOption { // LoggerOption implements the functional parameter paradigm for // SetLoggerWithOptions. -// -// Experimental -// -// Notice: This type is EXPERIMENTAL and may be changed or removed in a -// later release. type LoggerOption func(o *loggerOptions) type loggerOptions struct { @@ -164,11 +144,6 @@ func ClearLogger() { // to avoid the additional overhead for contextual logging. // // This must be called during initialization before goroutines are started. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func EnableContextualLogging(enabled bool) { contextualLoggingEnabled = enabled } @@ -176,11 +151,6 @@ func EnableContextualLogging(enabled bool) { // FromContext retrieves a logger set by the caller or, if not set, // falls back to the program's global logger (a Logger instance or klog // itself). -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func FromContext(ctx context.Context) Logger { if contextualLoggingEnabled { if logger, err := logr.FromContext(ctx); err == nil { @@ -194,11 +164,6 @@ func FromContext(ctx context.Context) Logger { // TODO can be used as a last resort by code that has no means of // receiving a logger from its caller. FromContext or an explicit logger // parameter should be used instead. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func TODO() Logger { return Background() } @@ -207,11 +172,6 @@ func TODO() Logger { // that logger was initialized by the program and not by code that should // better receive a logger via its parameters. TODO can be used as a temporary // solution for such code. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func Background() Logger { if globalLoggerOptions.contextualLogger { // Is non-nil because globalLoggerOptions.contextualLogger is @@ -224,11 +184,6 @@ func Background() Logger { // LoggerWithValues returns logger.WithValues(...kv) when // contextual logging is enabled, otherwise the logger. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func LoggerWithValues(logger Logger, kv ...interface{}) Logger { if contextualLoggingEnabled { return logger.WithValues(kv...) @@ -238,11 +193,6 @@ func LoggerWithValues(logger Logger, kv ...interface{}) Logger { // LoggerWithName returns logger.WithName(name) when contextual logging is // enabled, otherwise the logger. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func LoggerWithName(logger Logger, name string) Logger { if contextualLoggingEnabled { return logger.WithName(name) @@ -252,11 +202,6 @@ func LoggerWithName(logger Logger, name string) Logger { // NewContext returns logr.NewContext(ctx, logger) when // contextual logging is enabled, otherwise ctx. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func NewContext(ctx context.Context, logger Logger) context.Context { if contextualLoggingEnabled { return logr.NewContext(ctx, logger) diff --git a/imports.go b/imports.go index 43cd08190..602c3ed9e 100644 --- a/imports.go +++ b/imports.go @@ -24,35 +24,15 @@ import ( // without directly importing it. // Logger in this package is exactly the same as logr.Logger. -// -// Experimental -// -// Notice: This type is EXPERIMENTAL and may be changed or removed in a -// later release. type Logger = logr.Logger // LogSink in this package is exactly the same as logr.LogSink. -// -// Experimental -// -// Notice: This type is EXPERIMENTAL and may be changed or removed in a -// later release. type LogSink = logr.LogSink // Runtimeinfo in this package is exactly the same as logr.RuntimeInfo. -// -// Experimental -// -// Notice: This type is EXPERIMENTAL and may be changed or removed in a -// later release. type RuntimeInfo = logr.RuntimeInfo var ( // New is an alias for logr.New. - // - // Experimental - // - // Notice: This variable is EXPERIMENTAL and may be changed or removed in a - // later release. New = logr.New ) diff --git a/klogr.go b/klogr.go index cdb3834fa..351d7a740 100644 --- a/klogr.go +++ b/klogr.go @@ -25,11 +25,6 @@ import ( // NewKlogr returns a logger that is functionally identical to // klogr.NewWithOptions(klogr.FormatKlog), i.e. it passes through to klog. The // difference is that it uses a simpler implementation. -// -// Experimental -// -// Notice: This function is EXPERIMENTAL and may be changed or removed in a -// later release. func NewKlogr() Logger { return New(&klogger{}) } From 93980ca17100b7d547e2819c844fc8ff0d9a284c Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Feb 2022 09:56:25 +0100 Subject: [PATCH 061/125] logcheck: add test cases for parameter check The feature was implemented without adding those. We need tests to avoid regressions. --- hack/tools/logcheck/main_test.go | 5 ++ .../testdata/src/parameters/parameters.go | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 hack/tools/logcheck/testdata/src/parameters/parameters.go diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index 3e0c08ae3..e09ca0ae4 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -38,6 +38,11 @@ func TestAnalyzer(t *testing.T) { allowUnstructured: "false", testPackage: "doNotAllowUnstructuredLogs", }, + { + name: "Function call parameters", + allowUnstructured: "true", + testPackage: "parameters", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/hack/tools/logcheck/testdata/src/parameters/parameters.go b/hack/tools/logcheck/testdata/src/parameters/parameters.go new file mode 100644 index 000000000..c0328d67a --- /dev/null +++ b/hack/tools/logcheck/testdata/src/parameters/parameters.go @@ -0,0 +1,55 @@ +/* +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 parameter +// checking. + +package parameters + +import ( + "fmt" + + klog "k8s.io/klog/v2" +) + +func parameterCalls() { + // format strings (incomplete list...) + klog.Infof("%d", 1) + klog.InfoS("%d", 1) // want `structured logging function "InfoS" should not use format specifier "%d"` + klog.Info("%d", 1) // TODO: not detected + klog.Infoln("%d", 1) // TODO: not detected + klog.V(1).Infof("%d", 1) + klog.V(1).InfoS("%d", 1) // want `structured logging function "InfoS" should not use format specifier "%d"` + klog.V(1).Info("%d", 1) // TODO: not detected + klog.V(1).Infoln("%d", 1) // TODO: not detected + klog.Errorf("%d", 1) + klog.ErrorS(nil, "%d", 1) // want `structured logging function "ErrorS" should not use format specifier "%d"` + klog.Error("%d", 1) // TODO: not detected + klog.Errorln("%d", 1) // TODO: not detected + + klog.InfoS("hello", "value", fmt.Sprintf("%d", 1)) + + // odd number of parameters + klog.InfoS("hello", "key") // want `Additional arguments to InfoS should always be Key Value pairs. Please check if there is any key or value missing.` + klog.ErrorS(nil, "hello", "key") // want `Additional arguments to ErrorS should always be Key Value pairs. Please check if there is any key or value missing.` + + // non-string keys + klog.InfoS("hello", "1", 2) + klog.InfoS("hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` + klog.ErrorS(nil, "hello", "1", 2) + klog.ErrorS(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` +} From 0c36cf77ff8f2d3718e34f183a57113a362b089f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 24 Jan 2022 16:49:43 +0100 Subject: [PATCH 062/125] logcheck: move into package The motivation is that at some point in the future this may allow linking the linter into golangci. --- hack/tools/logcheck/main.go | 194 +------------------------ hack/tools/logcheck/main_test.go | 4 +- hack/tools/logcheck/pkg/logcheck.go | 211 ++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 192 deletions(-) create mode 100644 hack/tools/logcheck/pkg/logcheck.go diff --git a/hack/tools/logcheck/main.go b/hack/tools/logcheck/main.go index ffafecdbd..de5fd7e4b 100644 --- a/hack/tools/logcheck/main.go +++ b/hack/tools/logcheck/main.go @@ -17,199 +17,11 @@ limitations under the License. package main import ( - "flag" - "fmt" - "go/ast" - "go/token" - "strings" - - "golang.org/x/exp/utf8string" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/singlechecker" -) -type config struct { - // When enabled, logcheck will ignore calls to unstructured klog methods (Info, Infof, Error, Errorf, Warningf, etc) - allowUnstructured bool -} + "k8s.io/klog/hack/tools/logcheck/pkg" +) func main() { - 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, c *config) (interface{}, error) { - - for _, file := range pass.Files { - - ast.Inspect(file, func(n ast.Node) bool { - - // 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, c) - } - - return true - }) - } - return nil, nil -} - -// checkForFunctionExpr checks for unstructured logging function, prints error if found any. -func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { - - fun := fexpr.Fun - args := fexpr.Args - - /* we are extracting external package function calls e.g. klog.Infof fmt.Printf - and eliminating calls like setLocalHost() - basically function calls that has selector expression like . - */ - if selExpr, ok := fun.(*ast.SelectorExpr); ok { - // extracting function Name like Infof - fName := selExpr.Sel.Name - - // for nested function cases klog.V(1).Infof scenerios - // if selExpr.X contains one more caller expression which is selector expression - // we are extracting klog and discarding V(1) - if n, ok := selExpr.X.(*ast.CallExpr); ok { - if _, ok = n.Fun.(*ast.SelectorExpr); ok { - selExpr = n.Fun.(*ast.SelectorExpr) - } - } - - // extracting package name - pName, ok := selExpr.X.(*ast.Ident) - - if ok && pName.Name == "klog" { - // Matching if any unstructured logging function is used. - if !isUnstructured((fName)) { - // if format specifier is used, check for arg length will most probably fail - // so check for format specifier first and skip if found - if checkForFormatSpecifier(fexpr, pass) { - return - } - if fName == "InfoS" { - isKeysValid(args[1:], fun, pass, fName) - } else if fName == "ErrorS" { - isKeysValid(args[2:], fun, pass, fName) - } - } else if !c.allowUnstructured { - msg := fmt.Sprintf("unstructured logging function %q should not be used", fName) - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: msg, - }) - } - } - } -} - -func isUnstructured(fName string) bool { - - // List of klog functions we do not want to use after migration to structured logging. - unstrucured := []string{ - "Infof", "Info", "Infoln", "InfoDepth", - "Warning", "Warningf", "Warningln", "WarningDepth", - "Error", "Errorf", "Errorln", "ErrorDepth", - "Fatal", "Fatalf", "Fatalln", "FatalDepth", - "Exit", "Exitf", "Exitln", "ExitDepth", - } - - for _, name := range unstrucured { - if fName == name { - return true - } - } - - return false -} - -// isKeysValid check if all keys in keyAndValues is string type -func isKeysValid(keyValues []ast.Expr, fun ast.Expr, pass *analysis.Pass, funName string) { - if len(keyValues)%2 != 0 { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Additional arguments to %s should always be Key Value pairs. Please check if there is any key or value missing.", funName), - }) - return - } - - for index, arg := range keyValues { - if index%2 != 0 { - continue - } - lit, ok := arg.(*ast.BasicLit) - if !ok { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", arg), - }) - continue - } - if lit.Kind != token.STRING { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", lit.Value), - }) - continue - } - isASCII := utf8string.NewString(lit.Value).IsASCII() - if !isASCII { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments %s are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.", lit.Value), - }) - } - } -} - -func checkForFormatSpecifier(expr *ast.CallExpr, pass *analysis.Pass) bool { - if selExpr, ok := expr.Fun.(*ast.SelectorExpr); ok { - // extracting function Name like Infof - fName := selExpr.Sel.Name - if specifier, found := hasFormatSpecifier(expr.Args); found { - msg := fmt.Sprintf("structured logging function %q should not use format specifier %q", fName, specifier) - pass.Report(analysis.Diagnostic{ - Pos: expr.Fun.Pos(), - Message: msg, - }) - return true - } - } - return false -} - -func hasFormatSpecifier(fArgs []ast.Expr) (string, bool) { - formatSpecifiers := []string{ - "%v", "%+v", "%#v", "%T", - "%t", "%b", "%c", "%d", "%o", "%O", "%q", "%x", "%X", "%U", - "%e", "%E", "%f", "%F", "%g", "%G", "%s", "%q", "%p", - } - for _, fArg := range fArgs { - if arg, ok := fArg.(*ast.BasicLit); ok { - for _, specifier := range formatSpecifiers { - if strings.Contains(arg.Value, specifier) { - return specifier, true - } - } - } - } - return "", false + singlechecker.Main(pkg.Analyser()) } diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index e09ca0ae4..ea421758f 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -20,6 +20,8 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" + + "k8s.io/klog/hack/tools/logcheck/pkg" ) func TestAnalyzer(t *testing.T) { @@ -46,7 +48,7 @@ func TestAnalyzer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - analyzer := analyser() + analyzer := pkg.Analyser() analyzer.Flags.Set("allow-unstructured", tt.allowUnstructured) analysistest.Run(t, analysistest.TestData(), analyzer, tt.testPackage) }) diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go new file mode 100644 index 000000000..918d73ad9 --- /dev/null +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -0,0 +1,211 @@ +/* +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 pkg + +import ( + "flag" + "fmt" + "go/ast" + "go/token" + "strings" + + "golang.org/x/exp/utf8string" + "golang.org/x/tools/go/analysis" +) + +type config struct { + // When enabled, logcheck will ignore calls to unstructured klog methods (Info, Infof, Error, Errorf, Warningf, etc) + allowUnstructured bool +} + +// Analyser creates a new logcheck 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, c *config) (interface{}, error) { + + for _, file := range pass.Files { + + ast.Inspect(file, func(n ast.Node) bool { + + // 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, c) + } + + return true + }) + } + return nil, nil +} + +// checkForFunctionExpr checks for unstructured logging function, prints error if found any. +func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { + + fun := fexpr.Fun + args := fexpr.Args + + /* we are extracting external package function calls e.g. klog.Infof fmt.Printf + and eliminating calls like setLocalHost() + basically function calls that has selector expression like . + */ + if selExpr, ok := fun.(*ast.SelectorExpr); ok { + // extracting function Name like Infof + fName := selExpr.Sel.Name + + // for nested function cases klog.V(1).Infof scenerios + // if selExpr.X contains one more caller expression which is selector expression + // we are extracting klog and discarding V(1) + if n, ok := selExpr.X.(*ast.CallExpr); ok { + if _, ok = n.Fun.(*ast.SelectorExpr); ok { + selExpr = n.Fun.(*ast.SelectorExpr) + } + } + + // extracting package name + pName, ok := selExpr.X.(*ast.Ident) + + if ok && pName.Name == "klog" { + // Matching if any unstructured logging function is used. + if !isUnstructured((fName)) { + // if format specifier is used, check for arg length will most probably fail + // so check for format specifier first and skip if found + if checkForFormatSpecifier(fexpr, pass) { + return + } + if fName == "InfoS" { + isKeysValid(args[1:], fun, pass, fName) + } else if fName == "ErrorS" { + isKeysValid(args[2:], fun, pass, fName) + } + } else if !c.allowUnstructured { + msg := fmt.Sprintf("unstructured logging function %q should not be used", fName) + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: msg, + }) + } + } + } +} + +func isUnstructured(fName string) bool { + + // List of klog functions we do not want to use after migration to structured logging. + unstrucured := []string{ + "Infof", "Info", "Infoln", "InfoDepth", + "Warning", "Warningf", "Warningln", "WarningDepth", + "Error", "Errorf", "Errorln", "ErrorDepth", + "Fatal", "Fatalf", "Fatalln", "FatalDepth", + "Exit", "Exitf", "Exitln", "ExitDepth", + } + + for _, name := range unstrucured { + if fName == name { + return true + } + } + + return false +} + +// isKeysValid check if all keys in keyAndValues is string type +func isKeysValid(keyValues []ast.Expr, fun ast.Expr, pass *analysis.Pass, funName string) { + if len(keyValues)%2 != 0 { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("Additional arguments to %s should always be Key Value pairs. Please check if there is any key or value missing.", funName), + }) + return + } + + for index, arg := range keyValues { + if index%2 != 0 { + continue + } + lit, ok := arg.(*ast.BasicLit) + if !ok { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", arg), + }) + continue + } + if lit.Kind != token.STRING { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", lit.Value), + }) + continue + } + isASCII := utf8string.NewString(lit.Value).IsASCII() + if !isASCII { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("Key positional arguments %s are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.", lit.Value), + }) + } + } +} + +func checkForFormatSpecifier(expr *ast.CallExpr, pass *analysis.Pass) bool { + if selExpr, ok := expr.Fun.(*ast.SelectorExpr); ok { + // extracting function Name like Infof + fName := selExpr.Sel.Name + if specifier, found := hasFormatSpecifier(expr.Args); found { + msg := fmt.Sprintf("structured logging function %q should not use format specifier %q", fName, specifier) + pass.Report(analysis.Diagnostic{ + Pos: expr.Fun.Pos(), + Message: msg, + }) + return true + } + } + return false +} + +func hasFormatSpecifier(fArgs []ast.Expr) (string, bool) { + formatSpecifiers := []string{ + "%v", "%+v", "%#v", "%T", + "%t", "%b", "%c", "%d", "%o", "%O", "%q", "%x", "%X", "%U", + "%e", "%E", "%f", "%F", "%g", "%G", "%s", "%q", "%p", + } + for _, fArg := range fArgs { + if arg, ok := fArg.(*ast.BasicLit); ok { + for _, specifier := range formatSpecifiers { + if strings.Contains(arg.Value, specifier) { + return specifier, true + } + } + } + } + return "", false +} From 3f08024be738bb4319e81380f0a2232e3a2ada15 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 16 Feb 2022 10:20:04 +0100 Subject: [PATCH 063/125] logcheck: also check for format specifier in calls like klog.Info Not all of the structured logging calls in klog handle format strings. This check finds errors in Kubernetes like this one: cmd/kubeadm/app/util/users/users_linux.go:152:3: logging function "Info" should not use format specifier "%v" (logcheck) klog.V(1).Info("Could not open %q, using default system limits: %v", pathLoginDef, err) --- hack/tools/logcheck/pkg/logcheck.go | 9 ++++++++- .../allowUnstructuredLogs.go | 4 ++-- .../doNotAllowUnstructuredLogs.go | 4 ++-- .../testdata/src/parameters/parameters.go | 18 +++++++++--------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 918d73ad9..62720170c 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -112,6 +112,9 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { Pos: fun.Pos(), Message: msg, }) + } else { + // Also check non-structured calls. + checkForFormatSpecifier(fexpr, pass) } } } @@ -180,8 +183,12 @@ func checkForFormatSpecifier(expr *ast.CallExpr, pass *analysis.Pass) bool { if selExpr, ok := expr.Fun.(*ast.SelectorExpr); ok { // extracting function Name like Infof fName := selExpr.Sel.Name + if strings.HasSuffix(fName, "f") { + // Allowed for calls like Infof. + return false + } if specifier, found := hasFormatSpecifier(expr.Args); found { - msg := fmt.Sprintf("structured logging function %q should not use format specifier %q", fName, specifier) + msg := fmt.Sprintf("logging function %q should not use format specifier %q", fName, specifier) pass.Report(analysis.Diagnostic{ Pos: expr.Fun.Pos(), Message: msg, diff --git a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go index 879e5cf56..045eb8d10 100644 --- a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go +++ b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go @@ -39,8 +39,8 @@ func allowUnstructuredLogs() { 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"` + klog.InfoS("test: %s", "testname") // want `logging function "InfoS" should not use format specifier "%s"` + klog.ErrorS(nil, "test no.: %d", 1) // want `logging function "ErrorS" should not use format specifier "%d"` // Unstructured logs // Error is not expected as this package allows unstructured logging diff --git a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go index f4a6eebc3..f03e0e989 100644 --- a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go +++ b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go @@ -39,8 +39,8 @@ func doNotAllowUnstructuredLogs() { 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"` + klog.InfoS("test: %s", "testname") // want `logging function "InfoS" should not use format specifier "%s"` + klog.ErrorS(nil, "test no.: %d", 1) // want `logging function "ErrorS" should not use format specifier "%d"` // Unstructured logs // Error is expected as this package does not allow unstructured logging diff --git a/hack/tools/logcheck/testdata/src/parameters/parameters.go b/hack/tools/logcheck/testdata/src/parameters/parameters.go index c0328d67a..919870f1e 100644 --- a/hack/tools/logcheck/testdata/src/parameters/parameters.go +++ b/hack/tools/logcheck/testdata/src/parameters/parameters.go @@ -29,17 +29,17 @@ import ( func parameterCalls() { // format strings (incomplete list...) klog.Infof("%d", 1) - klog.InfoS("%d", 1) // want `structured logging function "InfoS" should not use format specifier "%d"` - klog.Info("%d", 1) // TODO: not detected - klog.Infoln("%d", 1) // TODO: not detected + klog.InfoS("%d", 1) // want `logging function "InfoS" should not use format specifier "%d"` + klog.Info("%d", 1) // want `logging function "Info" should not use format specifier "%d"` + klog.Infoln("%d", 1) // want `logging function "Infoln" should not use format specifier "%d"` klog.V(1).Infof("%d", 1) - klog.V(1).InfoS("%d", 1) // want `structured logging function "InfoS" should not use format specifier "%d"` - klog.V(1).Info("%d", 1) // TODO: not detected - klog.V(1).Infoln("%d", 1) // TODO: not detected + klog.V(1).InfoS("%d", 1) // want `logging function "InfoS" should not use format specifier "%d"` + klog.V(1).Info("%d", 1) // want `logging function "Info" should not use format specifier "%d"` + klog.V(1).Infoln("%d", 1) // want `logging function "Infoln" should not use format specifier "%d"` klog.Errorf("%d", 1) - klog.ErrorS(nil, "%d", 1) // want `structured logging function "ErrorS" should not use format specifier "%d"` - klog.Error("%d", 1) // TODO: not detected - klog.Errorln("%d", 1) // TODO: not detected + klog.ErrorS(nil, "%d", 1) // want `logging function "ErrorS" should not use format specifier "%d"` + klog.Error("%d", 1) // want `logging function "Error" should not use format specifier "%d"` + klog.Errorln("%d", 1) // want `logging function "Errorln" should not use format specifier "%d"` klog.InfoS("hello", "value", fmt.Sprintf("%d", 1)) From df531eb1c860f7ab9124f44976c3c5a8c542e579 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 25 Jan 2022 10:43:05 +0100 Subject: [PATCH 064/125] logcheck: support running as golangci-lint plugin Running the linter as part of golangci-lint has several advantages: - faster checking because finding files and parsing is shared with other linters - allows Kubernetes to get rid of the complex and buggy hack/verify-structured-logging.sh (https://github.com/kubernetes/kubernetes/issues/106746) - support for // nolint:logcheck For this to work, some changes are necessary: - support env variables for configuration (plugins cannot be configured via .golangci-lint.yaml) - per-file overrides for the overall "allow unstructured" flag The plugin code itself is trivial. --- hack/tools/go.mod | 3 +- hack/tools/go.sum | 1136 ++++++++++++++++- hack/tools/logcheck/README.md | 70 +- hack/tools/logcheck/main_test.go | 56 +- hack/tools/logcheck/pkg/filter.go | 116 ++ hack/tools/logcheck/pkg/filter_test.go | 157 +++ hack/tools/logcheck/pkg/logcheck.go | 103 +- hack/tools/logcheck/plugin/plugin.go | 35 + .../testdata/src/doNotAllowKlog/klog_logging | 1 + .../src/mixed/allowUnstructuredLogs.go | 31 + .../src/mixed/doNotAllowUnstructuredLogs.go | 30 + .../testdata/src/mixed/structured_logging | 6 + 12 files changed, 1693 insertions(+), 51 deletions(-) create mode 100644 hack/tools/logcheck/pkg/filter.go create mode 100644 hack/tools/logcheck/pkg/filter_test.go create mode 100644 hack/tools/logcheck/plugin/plugin.go create mode 100644 hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging create mode 100644 hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go create mode 100644 hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go create mode 100644 hack/tools/logcheck/testdata/src/mixed/structured_logging diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 3c94929c9..3b707b1c3 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -3,6 +3,7 @@ module k8s.io/klog/hack/tools go 1.15 require ( + github.com/golangci/golangci-lint v1.43.0 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f - golang.org/x/tools v0.1.0 + golang.org/x/tools v0.1.7 ) diff --git a/hack/tools/go.sum b/hack/tools/go.sum index e89e59cf9..8f9118e80 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -1,46 +1,1176 @@ +4d63.com/gochecknoglobals v0.1.0/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= +bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= +cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= +github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= +github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= +github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= +github.com/blizzy78/varnamelen v0.3.0/go.mod h1:hbwRdBvoBqxk34XyQ6HA0UH3G0/1TKuv5AC4eaBT0Ec= +github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= +github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= +github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= +github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.43.0 h1:SLwZFEmDgopqZpfP495zCtV9REUf551JJlJ51Ql7NZA= +github.com/golangci/golangci-lint v1.43.0/go.mod h1:VIFlUqidx5ggxDfQagdvd9E67UjMXtTHBkBQ7sHoC5Q= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= +github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= +github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw= +github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= +github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= +github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= +github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= +github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= +github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= +github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= +github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= +github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= +github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8BfaIeMzgBNKvc= +github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= +github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= +github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30= +github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ= +github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/go-ruleguard/dsl v0.3.10/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= +github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= +github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= +github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= +github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= +github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= +github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= +github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= +github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20210220032938-85be41e4509f h1:GrkO5AtFUU9U/1f5ctbIBXtBGeSJbWwIYfIsTcFMaX4= golang.org/x/exp v0.0.0-20210220032938-85be41e4509f/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= +mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/hack/tools/logcheck/README.md b/hack/tools/logcheck/README.md index 008231cc6..9466ca352 100644 --- a/hack/tools/logcheck/README.md +++ b/hack/tools/logcheck/README.md @@ -1,5 +1,69 @@ -This directory contains tool for checking use of unstructured logs in a package. It is created to prevent regression after packages have been migrated to use structured logs. +This directory contains a linter for checking log calls. It was originally +created to detect when unstructured logging calls like `klog.Infof` get added +to files that should only use structured logging calls like `klog.InfoS` +and now also supports other checks. -**Installation:**`go install k8s.io/klog/hack/tools/logcheck` -**Usage:** `$logcheck.go ` +# Installation + +`go install k8s.io/klog/hack/tools/logcheck` + +# Usage + +`$logcheck.go ` `e.g $logcheck ./pkg/kubelet/lifecycle/` + +# Configuration + +Checks can be enabled or disabled globally via command line flags and env +variables. In addition, the global setting for a check can be modified per file +via a configuration file. That file contains lines in this format: + +``` + +``` + +`` is a comma-separated list of the names of checks that get enabled or +disabled when a file name matches the regular expression. A check gets disabled +when its name has `-` as prefix and enabled when there is no prefix or `+` as +prefix. Only checks that are mentioned explicitly are modified. All regular +expressions are checked in order, so later lines can override the previous +ones. + +In this example, checking for klog calls is enabled for all files under +`pkg/scheduler` in the Kubernetes repo except for `scheduler.go` +itself. Parameter checking is disabled everywhere. + +``` +klog,-parameters k8s.io/kubernetes/pkg/scheduler/.* +-klog k8s.io/kubernetes/pkg/scheduler/scheduler.go +``` + +The names of all supported checks are the ones used as sub-section titles in +the next section. + +# Checks + +## structured (enabled by default) + +Unstructured klog logging calls are flagged as error. + +## parameters (enabled by default) + +This ensures that if certain logging functions are allowed and are used, those +functions are passed correct parameters. + +### all calls + +Format strings are not allowed where plain strings are expected. + +### structured logging calls + +Key/value parameters for logging calls are checked: +- For each key there must be a value. +- Keys must be constant strings. + +This also warns about code that is valid, for example code that collects +key/value pairs in an `[]interface` variable before passing that on to a log +call. Such valid code can use `nolint:logcheck` to disable the warning (when +invoking logcheck through golangci-lint) or the `parameters` check can be +disabled for the file. diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index ea421758f..3b98b3f4c 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -26,31 +26,55 @@ import ( func TestAnalyzer(t *testing.T) { tests := []struct { - name string - allowUnstructured string - testPackage string + name string + enabled map[string]string + override string + testPackage string }{ { - name: "Allow unstructured logs", - allowUnstructured: "true", - testPackage: "allowUnstructuredLogs", + name: "Allow unstructured logs", + enabled: map[string]string{ + "structured": "false", + }, + testPackage: "allowUnstructuredLogs", }, { - name: "Do not allow unstructured logs", - allowUnstructured: "false", - testPackage: "doNotAllowUnstructuredLogs", + name: "Do not allow unstructured logs", + testPackage: "doNotAllowUnstructuredLogs", }, { - name: "Function call parameters", - allowUnstructured: "true", - testPackage: "parameters", + name: "Per-file config", + enabled: map[string]string{ + "structured": "false", + }, + override: "testdata/src/mixed/structured_logging", + testPackage: "mixed", + }, + { + name: "Function call parameters", + enabled: map[string]string{ + "structured": "false", + }, + testPackage: "parameters", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { analyzer := pkg.Analyser() - analyzer.Flags.Set("allow-unstructured", tt.allowUnstructured) - analysistest.Run(t, analysistest.TestData(), analyzer, tt.testPackage) + set := func(flag, value string) { + if value != "" { + if err := analyzer.Flags.Set(flag, value); err != nil { + t.Fatalf("unexpected error for %s: %v", flag, err) + } + } + } + for key, value := range tc.enabled { + set("check-"+key, value) + } + if tc.override != "" { + set("config", tc.override) + } + analysistest.Run(t, analysistest.TestData(), analyzer, tc.testPackage) }) } } diff --git a/hack/tools/logcheck/pkg/filter.go b/hack/tools/logcheck/pkg/filter.go new file mode 100644 index 000000000..231a38cb8 --- /dev/null +++ b/hack/tools/logcheck/pkg/filter.go @@ -0,0 +1,116 @@ +/* +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 pkg + +import ( + "bufio" + "flag" + "fmt" + "os" + "regexp" + "strings" +) + +// RegexpFilter implements flag.Value by accepting a file name and parsing that +// file. +type RegexpFilter struct { + filename string + validChecks map[string]bool + lines []filter +} + +type filter struct { + enabled map[string]bool + match *regexp.Regexp +} + +var _ flag.Value = &RegexpFilter{} + +func (f *RegexpFilter) String() string { + return f.filename +} + +func (f *RegexpFilter) Set(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + // Reset before parsing. + f.filename = filename + f.lines = nil + + // Read line-by-line. + scanner := bufio.NewScanner(file) + for lineNr := 0; scanner.Scan(); lineNr++ { + text := scanner.Text() + if strings.HasPrefix(text, "#") { + continue + } + text = strings.TrimSpace(text) + if text == "" { + continue + } + + line := filter{ + enabled: map[string]bool{}, + } + parts := strings.SplitN(text, " ", 2) + if len(parts) != 2 { + return fmt.Errorf("%s:%d: not of the format : %s", filename, lineNr, text) + } + for _, c := range strings.Split(parts[0], ",") { + enabled := true + if strings.HasPrefix(c, "+") { + c = c[1:] + } else if strings.HasPrefix(c, "-") { + enabled = false + c = c[1:] + } + if !f.validChecks[c] { + return fmt.Errorf("%s:%d: %q is not a supported check: %s", filename, lineNr, c, text) + } + line.enabled[c] = enabled + } + + // Must match entire string. + re, err := regexp.Compile("^" + parts[1] + "$") + if err != nil { + return fmt.Errorf("%s:%d: %v", filename, lineNr, err) + } + line.match = re + f.lines = append(f.lines, line) + } + + if err := scanner.Err(); err != nil { + return err + } + return nil +} + +// Enabled checks whether a certain check is enabled for a file. +func (f *RegexpFilter) Enabled(check string, enabled bool, filename string) bool { + for _, l := range f.lines { + if l.match.MatchString(filename) { + if e, ok := l.enabled[check]; ok { + enabled = e + } + } + } + return enabled +} diff --git a/hack/tools/logcheck/pkg/filter_test.go b/hack/tools/logcheck/pkg/filter_test.go new file mode 100644 index 000000000..115af49af --- /dev/null +++ b/hack/tools/logcheck/pkg/filter_test.go @@ -0,0 +1,157 @@ +/* +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 pkg + +import ( + "io/ioutil" + "path" + "testing" +) + +func TestMatch(t *testing.T) { + temp := t.TempDir() + filename := path.Join(temp, "expressions") + if err := ioutil.WriteFile(filename, []byte(`# Example file +structured hello ++structured a.c +-structured adc +structured x.*y +structured,parameters world +`), 0666); err != nil { + t.Fatalf("writing file: %v", err) + } + + filter := &RegexpFilter{ + validChecks: map[string]bool{ + structuredCheck: true, + parametersCheck: true, + }, + } + if err := filter.Set(filename); err != nil { + t.Fatalf("reading file: %v", err) + } + + for _, tc := range []struct { + filename string + check string + enabled bool + expectEnabled bool + }{ + { + filename: "hello", + check: "structured", + expectEnabled: true, + }, + { + filename: "hello", + check: "parameters", + expectEnabled: false, // not set + }, + { + filename: "hello", + check: "parameters", + enabled: true, + expectEnabled: true, // global default + }, + { + filename: "hello/world", + check: "structured", + expectEnabled: false, // no sub-matches + }, + { + filename: "abc", + check: "structured", + expectEnabled: true, + }, + { + filename: "adc", + check: "structured", + expectEnabled: false, // unset later + }, + { + filename: "x1y", + check: "structured", + expectEnabled: true, + }, + { + filename: "x2y", + check: "structured", + expectEnabled: true, + }, + } { + actualEnabled := filter.Enabled(tc.check, tc.enabled, tc.filename) + if actualEnabled != tc.expectEnabled { + t.Errorf("%+v: got %v", tc, actualEnabled) + } + } +} + +func TestSetNoFile(t *testing.T) { + filter := &RegexpFilter{} + if err := filter.Set("no such file"); err == nil { + t.Errorf("did not get expected error") + } +} + +func TestParsing(t *testing.T) { + temp := t.TempDir() + filename := path.Join(temp, "expressions") + for name, tc := range map[string]struct { + content string + expectError string + }{ + "invalid-regexp": { + content: `structured [`, + expectError: filename + ":0: error parsing regexp: missing closing ]: `[$`", + }, + "invalid-line": { + content: `structured . +parameters`, + expectError: filename + ":1: not of the format : parameters", + }, + "invalid-check": { + content: `xxx .`, + expectError: filename + ":0: \"xxx\" is not a supported check: xxx .", + }, + } { + t.Run(name, func(t *testing.T) { + if err := ioutil.WriteFile(filename, []byte(tc.content), 0666); err != nil { + t.Fatalf("writing file: %v", err) + } + + filter := &RegexpFilter{ + validChecks: map[string]bool{ + structuredCheck: true, + parametersCheck: true, + }, + } + err := filter.Set(filename) + if tc.expectError == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil { + t.Fatalf("did not get expected error: %s", tc.expectError) + } + if err.Error() != tc.expectError { + t.Fatalf("error mismatch\nexpected: %q\n got: %q", tc.expectError, err.Error()) + } + } + }) + } +} diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 62720170c..a2713383c 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -21,23 +21,68 @@ import ( "fmt" "go/ast" "go/token" + "os" + "path" + "strconv" "strings" "golang.org/x/exp/utf8string" "golang.org/x/tools/go/analysis" ) +const ( + structuredCheck = "structured" + parametersCheck = "parameters" +) + +type checks map[string]*bool + type config struct { - // When enabled, logcheck will ignore calls to unstructured klog methods (Info, Infof, Error, Errorf, Warningf, etc) - allowUnstructured bool + enabled checks + fileOverrides RegexpFilter +} + +func (c config) isEnabled(check string, filename string) bool { + return c.fileOverrides.Enabled(check, *c.enabled[check], filename) } // Analyser creates a new logcheck analyser. func Analyser() *analysis.Analyzer { - c := config{} + c := config{ + enabled: checks{ + structuredCheck: new(bool), + parametersCheck: new(bool), + }, + } + c.fileOverrides.validChecks = map[string]bool{} + for key := range c.enabled { + c.fileOverrides.validChecks[key] = true + } 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)`) + prefix := "check-" + logcheckFlags.BoolVar(c.enabled[structuredCheck], prefix+structuredCheck, true, `When true, logcheck will warn about calls to unstructured +klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) + logcheckFlags.BoolVar(c.enabled[parametersCheck], prefix+parametersCheck, true, `When true, logcheck will check parameters of structured logging calls.`) + logcheckFlags.Var(&c.fileOverrides, "config", `A file which overrides the global settings for checks on a per-file basis via regular expressions.`) + + // Use env variables as defaults. This is necessary when used as plugin + // for golangci-lint because of + // https://github.com/golangci/golangci-lint/issues/1512. + for key, enabled := range c.enabled { + envVarName := "LOGCHECK_" + strings.ToUpper(strings.ReplaceAll(string(key), "-", "_")) + if value, ok := os.LookupEnv(envVarName); ok { + v, err := strconv.ParseBool(value) + if err != nil { + panic(fmt.Errorf("%s=%q: %v", envVarName, value, err)) + } + *enabled = v + } + } + if value, ok := os.LookupEnv("LOGCHECK_CONFIG"); ok { + if err := c.fileOverrides.Set(value); err != nil { + panic(fmt.Errorf("LOGCHECK_CONFIG=%q: %v", value, err)) + } + } return &analysis.Analyzer{ Name: "logcheck", @@ -50,11 +95,8 @@ klog methods (Info, Infof, Error, Errorf, Warningf, etc)`) } func run(pass *analysis.Pass, c *config) (interface{}, error) { - for _, file := range pass.Files { - ast.Inspect(file, func(n ast.Node) bool { - // 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 { @@ -69,7 +111,6 @@ func run(pass *analysis.Pass, c *config) (interface{}, error) { // checkForFunctionExpr checks for unstructured logging function, prints error if found any. func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { - fun := fexpr.Fun args := fexpr.Args @@ -81,6 +122,8 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { // extracting function Name like Infof fName := selExpr.Sel.Name + filename := pass.Pkg.Path() + "/" + path.Base(pass.Fset.Position(fexpr.Pos()).Filename) + // for nested function cases klog.V(1).Infof scenerios // if selExpr.X contains one more caller expression which is selector expression // we are extracting klog and discarding V(1) @@ -92,36 +135,40 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { // extracting package name pName, ok := selExpr.X.(*ast.Ident) - if ok && pName.Name == "klog" { // Matching if any unstructured logging function is used. if !isUnstructured((fName)) { - // if format specifier is used, check for arg length will most probably fail - // so check for format specifier first and skip if found - if checkForFormatSpecifier(fexpr, pass) { - return - } - if fName == "InfoS" { - isKeysValid(args[1:], fun, pass, fName) - } else if fName == "ErrorS" { - isKeysValid(args[2:], fun, pass, fName) + if c.isEnabled(parametersCheck, filename) { + // if format specifier is used, check for arg length will most probably fail + // so check for format specifier first and skip if found + if checkForFormatSpecifier(fexpr, pass) { + return + } + if fName == "InfoS" { + isKeysValid(args[1:], fun, pass, fName) + } else if fName == "ErrorS" { + isKeysValid(args[2:], fun, pass, fName) + } } - } else if !c.allowUnstructured { - msg := fmt.Sprintf("unstructured logging function %q should not be used", fName) - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: msg, - }) } else { - // Also check non-structured calls. - checkForFormatSpecifier(fexpr, pass) + if c.isEnabled(structuredCheck, filename) { + msg := fmt.Sprintf("unstructured logging function %q should not be used", fName) + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: msg, + }) + } + + // Also check structured calls. + if c.isEnabled(parametersCheck, filename) { + checkForFormatSpecifier(fexpr, pass) + } } } } } func isUnstructured(fName string) bool { - // List of klog functions we do not want to use after migration to structured logging. unstrucured := []string{ "Infof", "Info", "Infoln", "InfoDepth", diff --git a/hack/tools/logcheck/plugin/plugin.go b/hack/tools/logcheck/plugin/plugin.go new file mode 100644 index 000000000..cd3e04ce9 --- /dev/null +++ b/hack/tools/logcheck/plugin/plugin.go @@ -0,0 +1,35 @@ +/* +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 main is meant to be compiled as a plugin for golangci-lint, see +// https://golangci-lint.run/contributing/new-linters/#create-a-plugin. +package main + +import ( + "golang.org/x/tools/go/analysis" + "k8s.io/klog/hack/tools/logcheck/pkg" +) + +type analyzerPlugin struct{} + +func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { + return []*analysis.Analyzer{ + pkg.Analyser(), + } +} + +// AnalyzerPlugin is the entry point for golangci-lint. +var AnalyzerPlugin analyzerPlugin diff --git a/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging b/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging new file mode 100644 index 000000000..fd4e96afc --- /dev/null +++ b/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging @@ -0,0 +1 @@ +klog .*/doNotAllowKlog.go diff --git a/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go new file mode 100644 index 000000000..6215b2476 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go @@ -0,0 +1,31 @@ +/* +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 mixed + +import ( + klog "k8s.io/klog/v2" +) + +func allowUnstructuredLogs() { + // Error is not expected as this file allows unstructured logging + klog.Infof("test log") +} diff --git a/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go new file mode 100644 index 000000000..bd448e97c --- /dev/null +++ b/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go @@ -0,0 +1,30 @@ +/* +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 mixed + +import ( + klog "k8s.io/klog/v2" +) + +func dontAllowUnstructuredLogs() { + klog.Info("test log") // want `unstructured logging function "Info" should not be used` +} diff --git a/hack/tools/logcheck/testdata/src/mixed/structured_logging b/hack/tools/logcheck/testdata/src/mixed/structured_logging new file mode 100644 index 000000000..050a12714 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/mixed/structured_logging @@ -0,0 +1,6 @@ +# This file contains regular expressions that are matched against /, +# for example k8s.io/cmd/kube-scheduler/app/config/config.go. +# +# Any file that is matched may only use structured logging calls. + +structured .*doNotAllowUnstructuredLogs.go From c4c35ff8c3b86f767c5ac4e349134853152af828 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 25 Jan 2022 15:26:56 +0100 Subject: [PATCH 065/125] logcheck: support import renaming and improve klog.V(2) handling Importing klog under a different name was not supported because the name of the identifier was checked instead of the actual package: import glog "k8s.io/klog/v2" glog.Info("is unstructured") The hack for peeling the V(2) from a selector that has a CallExpr as expression was fragile. Checking the type against klog.Verbose is better and also covers this code fragment: if klogV := klog.V(2); klogV.Enabled) { klogV.Info("is unstructured") } --- hack/tools/logcheck/main_test.go | 8 ++++ hack/tools/logcheck/pkg/logcheck.go | 48 ++++++++++++++----- .../testdata/src/importrename/importrename.go | 30 ++++++++++++ .../testdata/src/k8s.io/klog/v2/klog.go | 5 ++ .../logcheck/testdata/src/verbose/verbose.go | 33 +++++++++++++ 5 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 hack/tools/logcheck/testdata/src/importrename/importrename.go create mode 100644 hack/tools/logcheck/testdata/src/verbose/verbose.go diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index 3b98b3f4c..60cb232c1 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -57,6 +57,14 @@ func TestAnalyzer(t *testing.T) { }, testPackage: "parameters", }, + { + name: "importrename", + testPackage: "importrename", + }, + { + name: "verbose", + testPackage: "verbose", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index a2713383c..b06426210 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -21,6 +21,7 @@ import ( "fmt" "go/ast" "go/token" + "go/types" "os" "path" "strconv" @@ -124,18 +125,8 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { filename := pass.Pkg.Path() + "/" + path.Base(pass.Fset.Position(fexpr.Pos()).Filename) - // for nested function cases klog.V(1).Infof scenerios - // if selExpr.X contains one more caller expression which is selector expression - // we are extracting klog and discarding V(1) - if n, ok := selExpr.X.(*ast.CallExpr); ok { - if _, ok = n.Fun.(*ast.SelectorExpr); ok { - selExpr = n.Fun.(*ast.SelectorExpr) - } - } - - // extracting package name - pName, ok := selExpr.X.(*ast.Ident) - if ok && pName.Name == "klog" { + // Now we need to determine whether it is coming from klog. + if isKlog(selExpr.X, pass) { // Matching if any unstructured logging function is used. if !isUnstructured((fName)) { if c.isEnabled(parametersCheck, filename) { @@ -168,6 +159,39 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { } } +// isKlog checks whether an expression is klog.Verbose or the klog package itself. +func isKlog(expr ast.Expr, pass *analysis.Pass) bool { + // For klog.V(1) and klogV := klog.V(1) we can decide based on the type. + if typeAndValue, ok := pass.TypesInfo.Types[expr]; ok { + switch t := typeAndValue.Type.(type) { + case *types.Named: + if typeName := t.Obj(); typeName != nil { + if pkg := typeName.Pkg(); pkg != nil { + if typeName.Name() == "Verbose" && pkg.Path() == "k8s.io/klog/v2" { + return true + } + } + } + } + } + + // In "klog.Info", "klog" is a package identifier. It doesn't need to + // be "klog" because here we look up the actual package. + if ident, ok := expr.(*ast.Ident); ok { + if object, ok := pass.TypesInfo.Uses[ident]; ok { + switch object := object.(type) { + case *types.PkgName: + pkg := object.Imported() + if pkg.Path() == "k8s.io/klog/v2" { + return true + } + } + } + } + + return false +} + func isUnstructured(fName string) bool { // List of klog functions we do not want to use after migration to structured logging. unstrucured := []string{ diff --git a/hack/tools/logcheck/testdata/src/importrename/importrename.go b/hack/tools/logcheck/testdata/src/importrename/importrename.go new file mode 100644 index 000000000..b5382bc60 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/importrename/importrename.go @@ -0,0 +1,30 @@ +/* +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 importrename + +import ( + glog "k8s.io/klog/v2" +) + +func dontAllowUnstructuredLogs() { + glog.Info("test log") // want `unstructured logging function "Info" 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 c43ee166b..20cf8949e 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 @@ -32,6 +32,11 @@ func V(level Level) Verbose { return Verbose{enabled: false} } +// Enabled returns true if logging at the selected level is enabled. +func (v Verbose) Enabled() bool { + return false +} + // Info is equivalent to the global Info function, guarded by the value of v. // See the documentation of V for usage. func (v Verbose) Info(args ...interface{}) { diff --git a/hack/tools/logcheck/testdata/src/verbose/verbose.go b/hack/tools/logcheck/testdata/src/verbose/verbose.go new file mode 100644 index 000000000..3fb759610 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/verbose/verbose.go @@ -0,0 +1,33 @@ +/* +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 verbose + +import ( + klog "k8s.io/klog/v2" +) + +func verboseLogging() { + klog.V(1).Info("test log") // want `unstructured logging function "Info" should not be used` + if klogV := klog.V(1); klogV.Enabled() { + klogV.Infof("hello %s", "world") // want `unstructured logging function "Infof" should not be used` + } +} From 567c037556d41854ea8e50bc1a2b4695c18bf196 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 25 Jan 2022 16:01:51 +0100 Subject: [PATCH 066/125] logcheck: support logr.Logger Key/value pairs for Logger.WithValues, Logger.Info and Logger.Error must pass the same sanity checks as the corresponding klog calls. --- hack/tools/logcheck/main_test.go | 4 ++ hack/tools/logcheck/pkg/logcheck.go | 31 ++++++++++++++- .../src/github.com/go-logr/logr/logr.go | 28 ++++++++++++++ .../logcheck/testdata/src/gologr/gologr.go | 38 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go create mode 100644 hack/tools/logcheck/testdata/src/gologr/gologr.go diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index 60cb232c1..579543716 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -65,6 +65,10 @@ func TestAnalyzer(t *testing.T) { name: "verbose", testPackage: "verbose", }, + { + name: "gologr", + testPackage: "gologr", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index b06426210..2ea59290b 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -87,7 +87,7 @@ klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) return &analysis.Analyzer{ Name: "logcheck", - Doc: "Tool to check use of unstructured logging patterns.", + Doc: "Tool to check logging calls.", Run: func(pass *analysis.Pass) (interface{}, error) { return run(pass, &c) }, @@ -155,6 +155,18 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { checkForFormatSpecifier(fexpr, pass) } } + } else if isGoLogger(selExpr.X, pass) { + if c.isEnabled(parametersCheck, filename) { + checkForFormatSpecifier(fexpr, pass) + switch fName { + case "WithValues": + isKeysValid(args, fun, pass, fName) + case "Info": + isKeysValid(args[1:], fun, pass, fName) + case "Error": + isKeysValid(args[2:], fun, pass, fName) + } + } } } } @@ -192,6 +204,23 @@ func isKlog(expr ast.Expr, pass *analysis.Pass) bool { return false } +// isGoLogger checks whether an expression is logr.Logger. +func isGoLogger(expr ast.Expr, pass *analysis.Pass) bool { + if typeAndValue, ok := pass.TypesInfo.Types[expr]; ok { + switch t := typeAndValue.Type.(type) { + case *types.Named: + if typeName := t.Obj(); typeName != nil { + if pkg := typeName.Pkg(); pkg != nil { + if typeName.Name() == "Logger" && pkg.Path() == "github.com/go-logr/logr" { + return true + } + } + } + } + } + return false +} + func isUnstructured(fName string) bool { // List of klog functions we do not want to use after migration to structured logging. unstrucured := []string{ diff --git a/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go b/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go new file mode 100644 index 000000000..aa54eb112 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go @@ -0,0 +1,28 @@ +/* +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 logr provides empty stubs for github.com/go-logr/logr for testing +// with golang.org/x/tools/go/analysis/analysistest. +package logr + +type Logger struct{} + +func (l Logger) Enabled() bool { return false } +func (l Logger) WithName(name string) Logger { return l } +func (l Logger) WithValues(kv ...interface{}) Logger { return l } +func (l Logger) V(level int) Logger { return l } +func (l Logger) Info(msg string, kv ...interface{}) {} +func (l Logger) Error(err error, msg string, kv ...interface{}) {} diff --git a/hack/tools/logcheck/testdata/src/gologr/gologr.go b/hack/tools/logcheck/testdata/src/gologr/gologr.go new file mode 100644 index 000000000..6d533bb1b --- /dev/null +++ b/hack/tools/logcheck/testdata/src/gologr/gologr.go @@ -0,0 +1,38 @@ +/* +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 gologr + +import ( + "github.com/go-logr/logr" +) + +var logger logr.Logger + +func logging() { + logger.Info("hello", "missing value") // want `Additional arguments to Info should always be Key Value pairs. Please check if there is any key or value missing.` + logger.Error(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` + logger.WithValues("missing value") // want `Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing.` + + logger.V(1).Info("hello", "missing value") // want `Additional arguments to Info should always be Key Value pairs. Please check if there is any key or value missing.` + logger.V(1).Error(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` + logger.V(1).WithValues("missing value") // want `Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing.` +} From 5b4c326b9f572f831e001798b0e69571b9acca66 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 26 Jan 2022 09:57:27 +0100 Subject: [PATCH 067/125] logcheck: warn about functions with both Logger and Context That leads to ambiguities when calling the function because it must be considered carefully whether the context must contain the logger. --- hack/tools/logcheck/main_test.go | 4 ++ hack/tools/logcheck/pkg/logcheck.go | 44 +++++++++++++++-- .../testdata/src/contextual/contextual.go | 49 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 hack/tools/logcheck/testdata/src/contextual/contextual.go diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index 579543716..1a46a8f27 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -69,6 +69,10 @@ func TestAnalyzer(t *testing.T) { name: "gologr", testPackage: "gologr", }, + { + name: "contextual", + testPackage: "contextual", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 2ea59290b..6f0a183eb 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -98,10 +98,13 @@ klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) func run(pass *analysis.Pass, c *config) (interface{}, error) { for _, file := range pass.Files { ast.Inspect(file, func(n ast.Node) bool { - // 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, c) + switch n := n.(type) { + case *ast.CallExpr: + // We are intrested in function calls, as we want to detect klog.* calls + // passing all function calls to checkForFunctionExpr + checkForFunctionExpr(n, pass, c) + case *ast.FuncType: + checkForContextAndLogger(n, n.Params, pass, c) } return true @@ -316,3 +319,36 @@ func hasFormatSpecifier(fArgs []ast.Expr) (string, bool) { } return "", false } + +// checkForContextAndLogger ensures that a function doesn't accept both a +// context and a logger. That is problematic because it leads to ambiguity: +// does the context already contain the logger? That matters when passing it on +// without the logger. +func checkForContextAndLogger(n ast.Node, params *ast.FieldList, pass *analysis.Pass, c *config) { + var haveLogger, haveContext bool + + for _, param := range params.List { + if typeAndValue, ok := pass.TypesInfo.Types[param.Type]; ok { + switch t := typeAndValue.Type.(type) { + case *types.Named: + if typeName := t.Obj(); typeName != nil { + if pkg := typeName.Pkg(); pkg != nil { + if typeName.Name() == "Logger" && pkg.Path() == "github.com/go-logr/logr" { + haveLogger = true + } else if typeName.Name() == "Context" && pkg.Path() == "context" { + haveContext = true + } + } + } + } + } + } + + if haveLogger && haveContext { + pass.Report(analysis.Diagnostic{ + Pos: n.Pos(), + End: n.End(), + Message: `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.`, + }) + } +} diff --git a/hack/tools/logcheck/testdata/src/contextual/contextual.go b/hack/tools/logcheck/testdata/src/contextual/contextual.go new file mode 100644 index 000000000..51053f6e6 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/contextual/contextual.go @@ -0,0 +1,49 @@ +/* +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 contextual + +import ( + "context" + + "github.com/go-logr/logr" +) + +type myFuncType func(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` + +func usingMyFuncType(firstParam int, + callback myFuncType, // Will be warned about at the type definition, not here. + lastParam int) { +} + +func usingInlineFunc(firstParam int, + callback func(ctx context.Context, logger logr.Logger, msg string), // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` + lastParam int) { +} + +type myStruct struct { + myFuncField func(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` +} + +func (m myStruct) myMethod(ctx context.Context, logger logr.Logger, msg string) { // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` +} + +func myFunction(ctx context.Context, logger logr.Logger, msg string) { // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` +} + +type myInterface interface { + myInterfaceMethod(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` +} From 41995aa7ff84b3f35a342b2523528448f9d34eff Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 26 Jan 2022 19:57:16 +0100 Subject: [PATCH 068/125] logcheck: detect if V().Enabled() Besides being slightly inefficient, not storing the result of V() in a variable is often also an indication for not using the right call inside the if block: if klog.V(2).Enabled() { klog.InfoS("some log message") } This records the message with v=0. Correct is: if klogV := klog.V(2); klogV.Enabled() { klogV.InfoS("some log message") } Detecting if clauses that do use this condition with a local variable but then incorrectly use different logger inside the if block is a separate problem and not solved yet. --- hack/tools/logcheck/pkg/logcheck.go | 82 ++++++++++++++++++- .../logcheck/testdata/src/verbose/verbose.go | 24 ++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 6f0a183eb..5ec101696 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -105,6 +105,8 @@ func run(pass *analysis.Pass, c *config) (interface{}, error) { checkForFunctionExpr(n, pass, c) case *ast.FuncType: checkForContextAndLogger(n, n.Params, pass, c) + case *ast.IfStmt: + checkForIfEnabled(n, pass, c) } return true @@ -174,9 +176,9 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { } } -// isKlog checks whether an expression is klog.Verbose or the klog package itself. -func isKlog(expr ast.Expr, pass *analysis.Pass) bool { - // For klog.V(1) and klogV := klog.V(1) we can decide based on the type. +// isKlogVerbose returns true if the type of the expression is klog.Verbose (= +// the result of klog.V). +func isKlogVerbose(expr ast.Expr, pass *analysis.Pass) bool { if typeAndValue, ok := pass.TypesInfo.Types[expr]; ok { switch t := typeAndValue.Type.(type) { case *types.Named: @@ -189,6 +191,15 @@ func isKlog(expr ast.Expr, pass *analysis.Pass) bool { } } } + return false +} + +// isKlog checks whether an expression is klog.Verbose or the klog package itself. +func isKlog(expr ast.Expr, pass *analysis.Pass) bool { + // For klog.V(1) and klogV := klog.V(1) we can decide based on the type. + if isKlogVerbose(expr, pass) { + return true + } // In "klog.Info", "klog" is a package identifier. It doesn't need to // be "klog" because here we look up the actual package. @@ -352,3 +363,68 @@ func checkForContextAndLogger(n ast.Node, params *ast.FieldList, pass *analysis. }) } } + +// checkForIfEnabled detects `if klog.V(..).Enabled() { ...` and `if +// logger.V(...).Enabled()` and suggests capturing the result of V. +func checkForIfEnabled(i *ast.IfStmt, pass *analysis.Pass, c *config) { + // if i.Init == nil { + // A more complex if statement, let's assume it's okay. + // return + // } + + // Must be a method call. + callExpr, ok := i.Cond.(*ast.CallExpr) + if !ok { + return + } + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return + } + + // We only care about calls to Enabled(). + if selExpr.Sel.Name != "Enabled" { + return + } + + // And it must be Enabled for klog or logr.Logger. + if !isKlogVerbose(selExpr.X, pass) && + !isGoLogger(selExpr.X, pass) { + return + } + + // logger.Enabled() is okay, logger.V(1).Enabled() is not. + // That means we need to check for another selector expression + // with V as method name. + subCallExpr, ok := selExpr.X.(*ast.CallExpr) + if !ok { + return + } + subSelExpr, ok := subCallExpr.Fun.(*ast.SelectorExpr) + if !ok || subSelExpr.Sel.Name != "V" { + return + } + + // klogV is recommended as replacement for klog.V(). For logr.Logger + // let's use the root of the selector, which should be a variable. + varName := "klogV" + funcCall := "klog.V" + if isGoLogger(subSelExpr.X, pass) { + varName = "logger" + root := subSelExpr + for s, ok := root.X.(*ast.SelectorExpr); ok; s, ok = root.X.(*ast.SelectorExpr) { + root = s + } + if id, ok := root.X.(*ast.Ident); ok { + varName = id.Name + } + funcCall = varName + ".V" + } + + pass.Report(analysis.Diagnostic{ + Pos: i.Pos(), + End: i.End(), + Message: fmt.Sprintf("The result of %s should be stored in a variable and then be used multiple times: if %s := %s(); %s.Enabled() { ... %s.Info ... }", + funcCall, varName, funcCall, varName, varName), + }) +} diff --git a/hack/tools/logcheck/testdata/src/verbose/verbose.go b/hack/tools/logcheck/testdata/src/verbose/verbose.go index 3fb759610..b49de0c9a 100644 --- a/hack/tools/logcheck/testdata/src/verbose/verbose.go +++ b/hack/tools/logcheck/testdata/src/verbose/verbose.go @@ -22,12 +22,36 @@ limitations under the License. package verbose import ( + "github.com/go-logr/logr" klog "k8s.io/klog/v2" ) +var l, logger logr.Logger + func verboseLogging() { klog.V(1).Info("test log") // want `unstructured logging function "Info" should not be used` if klogV := klog.V(1); klogV.Enabled() { klogV.Infof("hello %s", "world") // want `unstructured logging function "Infof" should not be used` } + + // \(\) is actually () in the diagnostic output. We have to escape here + // because `want` expects a regular expression. + + if klog.V(1).Enabled() { // want `The result of klog.V should be stored in a variable and then be used multiple times: if klogV := klog.V\(\); klogV.Enabled\(\) { ... klogV.Info ... }` + klog.V(1).InfoS("I'm logging at level 1.") + } + + if l.V(1).Enabled() { // want `The result of l.V should be stored in a variable and then be used multiple times: if l := l.V\(\); l.Enabled\(\) { ... l.Info ... }` + l.V(1).Info("I'm logging at level 1.") + } + + if l := l.V(2); l.Enabled() { + l.Info("I'm logging at level 2.") + } + + if l := logger.V(2); l.Enabled() { + // This is probably an error (should be l instead of logger), + // but not currently detected. + logger.Info("I wanted to log at level 2, but really it is 0.") + } } From 317fe52d105673e87bdbfe66994a720f621931ce Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 31 Jan 2022 17:14:42 +0100 Subject: [PATCH 069/125] logcheck: optionally warn about using direct klog logging This is even stricter than the check for unstructured klog functions: only contextual logging functions are allowed. merge: logcheck: optionally warn about using legacy klog logging --- hack/tools/logcheck/README.md | 6 ++ hack/tools/logcheck/main_test.go | 27 ++++++- hack/tools/logcheck/pkg/logcheck.go | 80 ++++++++++++++----- .../testdata/src/doNotAllowKlog/klog_logging | 1 - .../src/github.com/go-logr/logr/logr.go | 8 ++ .../testdata/src/k8s.io/klog/v2/klog.go | 46 +++++++++++ .../src/onlyAllowContextual/klog_logging | 1 + .../onlyAllowContextual.go | 32 ++++++++ .../onlyAllowContextual/whitelistedKlog.go | 37 +++++++++ 9 files changed, 216 insertions(+), 22 deletions(-) delete mode 100644 hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging create mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging create mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go create mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go diff --git a/hack/tools/logcheck/README.md b/hack/tools/logcheck/README.md index 9466ca352..83cc04409 100644 --- a/hack/tools/logcheck/README.md +++ b/hack/tools/logcheck/README.md @@ -47,6 +47,12 @@ the next section. Unstructured klog logging calls are flagged as error. +## klog (disabled by default) + +None of the klog logging methods may be used. This is even stricter than +`unstructured`. Instead, code should retrieve a logr.Logger from klog and log +through that. + ## parameters (enabled by default) This ensures that if certain logging functions are allowed and are used, those diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index 1a46a8f27..cf160a6a4 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -57,6 +57,23 @@ func TestAnalyzer(t *testing.T) { }, testPackage: "parameters", }, + { + name: "Only allow contextual calls", + enabled: map[string]string{ + "structured": "true", + "contextual": "true", + }, + testPackage: "onlyAllowContextual", + }, + { + name: "Only allow contextual calls through config", + enabled: map[string]string{ + "structured": "false", + "contextual": "false", + }, + override: "testdata/src/onlyAllowContextual/klog_logging", + testPackage: "onlyAllowContextual", + }, { name: "importrename", testPackage: "importrename", @@ -66,11 +83,17 @@ func TestAnalyzer(t *testing.T) { testPackage: "verbose", }, { - name: "gologr", + name: "gologr", + enabled: map[string]string{ + "contextual": "true", + }, testPackage: "gologr", }, { - name: "contextual", + name: "contextual", + enabled: map[string]string{ + "contextual": "true", + }, testPackage: "contextual", }, } diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 5ec101696..3512248da 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -34,6 +34,7 @@ import ( const ( structuredCheck = "structured" parametersCheck = "parameters" + contextualCheck = "contextual" ) type checks map[string]*bool @@ -53,6 +54,7 @@ func Analyser() *analysis.Analyzer { enabled: checks{ structuredCheck: new(bool), parametersCheck: new(bool), + contextualCheck: new(bool), }, } c.fileOverrides.validChecks = map[string]bool{} @@ -64,6 +66,7 @@ func Analyser() *analysis.Analyzer { logcheckFlags.BoolVar(c.enabled[structuredCheck], prefix+structuredCheck, true, `When true, logcheck will warn about calls to unstructured klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) logcheckFlags.BoolVar(c.enabled[parametersCheck], prefix+parametersCheck, true, `When true, logcheck will check parameters of structured logging calls.`) + logcheckFlags.BoolVar(c.enabled[contextualCheck], prefix+contextualCheck, false, `When true, logcheck will only allow log calls for contextual logging (retrieving a Logger from klog or the context and logging through that) and warn about all others.`) logcheckFlags.Var(&c.fileOverrides, "config", `A file which overrides the global settings for checks on a per-file basis via regular expressions.`) // Use env variables as defaults. This is necessary when used as plugin @@ -132,27 +135,33 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { // Now we need to determine whether it is coming from klog. if isKlog(selExpr.X, pass) { + if c.isEnabled(contextualCheck, filename) && !isContextualCall(fName) { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("function %q should not be used, convert to contextual logging", fName), + }) + return + } + // Matching if any unstructured logging function is used. - if !isUnstructured((fName)) { - if c.isEnabled(parametersCheck, filename) { - // if format specifier is used, check for arg length will most probably fail - // so check for format specifier first and skip if found - if checkForFormatSpecifier(fexpr, pass) { - return - } - if fName == "InfoS" { - isKeysValid(args[1:], fun, pass, fName) - } else if fName == "ErrorS" { - isKeysValid(args[2:], fun, pass, fName) - } + if c.isEnabled(structuredCheck, filename) && isUnstructured(fName) { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("unstructured logging function %q should not be used", fName), + }) + return + } + + if c.isEnabled(parametersCheck, filename) { + // if format specifier is used, check for arg length will most probably fail + // so check for format specifier first and skip if found + if checkForFormatSpecifier(fexpr, pass) { + return } - } else { - if c.isEnabled(structuredCheck, filename) { - msg := fmt.Sprintf("unstructured logging function %q should not be used", fName) - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: msg, - }) + if fName == "InfoS" { + isKeysValid(args[1:], fun, pass, fName) + } else if fName == "ErrorS" { + isKeysValid(args[2:], fun, pass, fName) } // Also check structured calls. @@ -254,6 +263,39 @@ func isUnstructured(fName string) bool { return false } +func isContextualCall(fName string) bool { + // List of klog functions we still want to use after migration to + // contextual logging. This is an allow list, so any new acceptable + // klog call has to be added here. + contextual := []string{ + "Background", + "ClearLogger", + "ContextualLogger", + "EnableContextualLogging", + "FlushAndExit", + "FlushLogger", + "FromContext", + "KObj", + "KObjs", + "KRef", + "LoggerWithName", + "LoggerWithValues", + "NewContext", + "SetLogger", + "SetLoggerWithOptions", + "StartFlushDaemon", + "StopFlushDaemon", + "TODO", + } + for _, name := range contextual { + if fName == name { + return true + } + } + + return false +} + // isKeysValid check if all keys in keyAndValues is string type func isKeysValid(keyValues []ast.Expr, fun ast.Expr, pass *analysis.Pass, funName string) { if len(keyValues)%2 != 0 { diff --git a/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging b/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging deleted file mode 100644 index fd4e96afc..000000000 --- a/hack/tools/logcheck/testdata/src/doNotAllowKlog/klog_logging +++ /dev/null @@ -1 +0,0 @@ -klog .*/doNotAllowKlog.go diff --git a/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go b/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go index aa54eb112..fa2da1e9d 100644 --- a/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go +++ b/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go @@ -18,6 +18,10 @@ limitations under the License. // with golang.org/x/tools/go/analysis/analysistest. package logr +import ( + "context" +) + type Logger struct{} func (l Logger) Enabled() bool { return false } @@ -26,3 +30,7 @@ func (l Logger) WithValues(kv ...interface{}) Logger { return l } func (l Logger) V(level int) Logger { return l } func (l Logger) Info(msg string, kv ...interface{}) {} func (l Logger) Error(err error, msg string, kv ...interface{}) {} + +func NewContext(ctx context.Context, logger Logger) context.Context { + return nil +} 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 20cf8949e..d62a5d433 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 @@ -19,6 +19,12 @@ limitations under the License. package v2 +import ( + "context" + + "github.com/go-logr/logr" +) + // Verbose is a boolean type that implements Infof (like Printf) etc. // See the documentation of V for more information. type Verbose struct { @@ -27,6 +33,8 @@ type Verbose struct { type Level int32 +type Logger = logr.Logger + func V(level Level) Verbose { return Verbose{enabled: false} @@ -202,3 +210,41 @@ func Exitln(args ...interface{}) { // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. func Exitf(format string, args ...interface{}) { } + +// KObj emulates klog.KObj +func KObj(obj interface{}) interface{} { + return nil +} + +// KObjs emulates klog.KObjs +func KObjs(obj interface{}) interface{} { + return nil +} + +func KRef(namespace, name string) interface{} { + return nil +} + +func FromContext(ctx context.Context) Logger { + return Logger{} +} + +func NewContext(ctx context.Context, logger Logger) context.Context { + return ctx +} + +func LoggerWithName(logger Logger, name string) Logger { + return Logger{} +} + +func LoggerWithValues(logger Logger, kvs ...interface{}) Logger { + return Logger{} +} + +func TODO() Logger { + return Logger{} +} + +func Background() Logger { + return Logger{} +} diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging b/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging new file mode 100644 index 000000000..d298e40aa --- /dev/null +++ b/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging @@ -0,0 +1 @@ +contextual .*onlyAllowContextual.go diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go b/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go new file mode 100644 index 000000000..4cc5e128e --- /dev/null +++ b/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.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. +*/ + +// 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 onlyallowcontextual + +import ( + klog "k8s.io/klog/v2" +) + +func doNotAlllowKlog() { + klog.InfoS("test log") // want `function "InfoS" should not be used, convert to contextual logging` + klog.ErrorS(nil, "test log") // want `function "ErrorS" should not be used, convert to contextual logging` + klog.V(1).Infof("test log") // want `function "V" should not be used, convert to contextual logging` `function "Infof" should not be used, convert to contextual logging` +} diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go b/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go new file mode 100644 index 000000000..07bb2a2a9 --- /dev/null +++ b/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go @@ -0,0 +1,37 @@ +/* +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 onlyallowcontextual + +import ( + klog "k8s.io/klog/v2" +) + +func allowKlog() { + klog.KObj(nil) + klog.KObjs(nil) + klog.KRef("", "") + klog.FromContext(nil) + klog.TODO() + klog.Background() + klog.LoggerWithName(klog.Logger{}, "foo") + klog.LoggerWithValues(klog.Logger{}, "a", "b") +} From 216a02ee228359072c705962c9342ae01b5d2c3c Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 2 Feb 2022 13:43:10 +0100 Subject: [PATCH 070/125] logcheck: add check for With* and NewContext The corresponding helper functions in klog should be used instead. This is needed for the feature gate in Kubernetes. --- hack/tools/logcheck/README.md | 7 +++ hack/tools/logcheck/main_test.go | 7 +++ hack/tools/logcheck/pkg/logcheck.go | 40 ++++++++++++++--- .../src/helpers/doNotAllowDirectCalls.go | 43 +++++++++++++++++++ 4 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go diff --git a/hack/tools/logcheck/README.md b/hack/tools/logcheck/README.md index 83cc04409..1e96afbcd 100644 --- a/hack/tools/logcheck/README.md +++ b/hack/tools/logcheck/README.md @@ -73,3 +73,10 @@ key/value pairs in an `[]interface` variable before passing that on to a log call. Such valid code can use `nolint:logcheck` to disable the warning (when invoking logcheck through golangci-lint) or the `parameters` check can be disabled for the file. + +## with-helpers (disabled by default) + +`logr.Logger.WithName`, `logr.Logger.WithValues` and `logr.NewContext` must not +be used. The corresponding helper calls from `k8s.io/klogr` should be used +instead. This is relevant when support contextual logging is disabled at +runtime in klog. diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go index cf160a6a4..e759154b5 100644 --- a/hack/tools/logcheck/main_test.go +++ b/hack/tools/logcheck/main_test.go @@ -96,6 +96,13 @@ func TestAnalyzer(t *testing.T) { }, testPackage: "contextual", }, + { + name: "helpers", + enabled: map[string]string{ + "with-helpers": "true", + }, + testPackage: "helpers", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 3512248da..139d471fa 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -32,9 +32,10 @@ import ( ) const ( - structuredCheck = "structured" - parametersCheck = "parameters" - contextualCheck = "contextual" + structuredCheck = "structured" + parametersCheck = "parameters" + contextualCheck = "contextual" + withHelpersCheck = "with-helpers" ) type checks map[string]*bool @@ -52,9 +53,10 @@ func (c config) isEnabled(check string, filename string) bool { func Analyser() *analysis.Analyzer { c := config{ enabled: checks{ - structuredCheck: new(bool), - parametersCheck: new(bool), - contextualCheck: new(bool), + structuredCheck: new(bool), + parametersCheck: new(bool), + contextualCheck: new(bool), + withHelpersCheck: new(bool), }, } c.fileOverrides.validChecks = map[string]bool{} @@ -67,6 +69,7 @@ func Analyser() *analysis.Analyzer { klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) logcheckFlags.BoolVar(c.enabled[parametersCheck], prefix+parametersCheck, true, `When true, logcheck will check parameters of structured logging calls.`) logcheckFlags.BoolVar(c.enabled[contextualCheck], prefix+contextualCheck, false, `When true, logcheck will only allow log calls for contextual logging (retrieving a Logger from klog or the context and logging through that) and warn about all others.`) + logcheckFlags.BoolVar(c.enabled[withHelpersCheck], prefix+withHelpersCheck, false, `When true, logcheck will warn about direct calls to WithName, WithValues and NewContext.`) logcheckFlags.Var(&c.fileOverrides, "config", `A file which overrides the global settings for checks on a per-file basis via regular expressions.`) // Use env variables as defaults. This is necessary when used as plugin @@ -181,7 +184,24 @@ func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { isKeysValid(args[2:], fun, pass, fName) } } + if c.isEnabled(withHelpersCheck, filename) { + switch fName { + case "WithValues", "WithName": + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("function %q should be called through klogr.Logger%s", fName, fName), + }) + } + } + } else if fName == "NewContext" && + isPackage(selExpr.X, "github.com/go-logr/logr", pass) && + c.isEnabled(withHelpersCheck, filename) { + pass.Report(analysis.Diagnostic{ + Pos: fun.Pos(), + Message: fmt.Sprintf("function %q should be called through klogr.NewContext", fName), + }) } + } } @@ -212,12 +232,18 @@ func isKlog(expr ast.Expr, pass *analysis.Pass) bool { // In "klog.Info", "klog" is a package identifier. It doesn't need to // be "klog" because here we look up the actual package. + return isPackage(expr, "k8s.io/klog/v2", pass) +} + +// isPackage checks whether an expression is an identifier that refers +// to a specific package like k8s.io/klog/v2. +func isPackage(expr ast.Expr, packagePath string, pass *analysis.Pass) bool { if ident, ok := expr.(*ast.Ident); ok { if object, ok := pass.TypesInfo.Uses[ident]; ok { switch object := object.(type) { case *types.PkgName: pkg := object.Imported() - if pkg.Path() == "k8s.io/klog/v2" { + if pkg.Path() == packagePath { return true } } diff --git a/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go b/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go new file mode 100644 index 000000000..22d8bb84d --- /dev/null +++ b/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go @@ -0,0 +1,43 @@ +/* +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 helpers + +import ( + "context" + + "github.com/go-logr/logr" + klog "k8s.io/klog/v2" +) + +var logger klog.Logger + +func doNotAlllowDirectCalls() { + logger.WithName("foo") // want `function "WithName" should be called through klogr.LoggerWithName` + logger.WithValues("a", "b") // want `function "WithValues" should be called through klogr.LoggerWithValues` + logr.NewContext(context.Background(), logger) // want `function "NewContext" should be called through klogr.NewContext` +} + +func allowHelpers() { + klog.LoggerWithName(logger, "foo") + klog.LoggerWithValues(logger, "a", "b") + klog.NewContext(context.Background(), logger) +} From 1876dee1655a7adda24ae9a38a38b75f3aa5ef4e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 1 Mar 2022 12:10:29 +0100 Subject: [PATCH 071/125] logcheck: harmonize report messages If the message consists of full sentences, it should use valid spelling (upper capital at the beginning, full stop at the end). Shorter messages should be consistent with messages from other tools where lower case seems to be more common (https://grep.app/search?q=pass.Report). --- hack/tools/logcheck/pkg/logcheck.go | 6 +++--- hack/tools/logcheck/testdata/src/verbose/verbose.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go index 139d471fa..38122de0e 100644 --- a/hack/tools/logcheck/pkg/logcheck.go +++ b/hack/tools/logcheck/pkg/logcheck.go @@ -340,14 +340,14 @@ func isKeysValid(keyValues []ast.Expr, fun ast.Expr, pass *analysis.Pass, funNam if !ok { pass.Report(analysis.Diagnostic{ Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", arg), + Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value.", arg), }) continue } if lit.Kind != token.STRING { pass.Report(analysis.Diagnostic{ Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value", lit.Value), + Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value.", lit.Value), }) continue } @@ -492,7 +492,7 @@ func checkForIfEnabled(i *ast.IfStmt, pass *analysis.Pass, c *config) { pass.Report(analysis.Diagnostic{ Pos: i.Pos(), End: i.End(), - Message: fmt.Sprintf("The result of %s should be stored in a variable and then be used multiple times: if %s := %s(); %s.Enabled() { ... %s.Info ... }", + Message: fmt.Sprintf("the result of %s should be stored in a variable and then be used multiple times: if %s := %s(); %s.Enabled() { ... %s.Info ... }", funcCall, varName, funcCall, varName, varName), }) } diff --git a/hack/tools/logcheck/testdata/src/verbose/verbose.go b/hack/tools/logcheck/testdata/src/verbose/verbose.go index b49de0c9a..fa8758c0b 100644 --- a/hack/tools/logcheck/testdata/src/verbose/verbose.go +++ b/hack/tools/logcheck/testdata/src/verbose/verbose.go @@ -37,11 +37,11 @@ func verboseLogging() { // \(\) is actually () in the diagnostic output. We have to escape here // because `want` expects a regular expression. - if klog.V(1).Enabled() { // want `The result of klog.V should be stored in a variable and then be used multiple times: if klogV := klog.V\(\); klogV.Enabled\(\) { ... klogV.Info ... }` + if klog.V(1).Enabled() { // want `the result of klog.V should be stored in a variable and then be used multiple times: if klogV := klog.V\(\); klogV.Enabled\(\) { ... klogV.Info ... }` klog.V(1).InfoS("I'm logging at level 1.") } - if l.V(1).Enabled() { // want `The result of l.V should be stored in a variable and then be used multiple times: if l := l.V\(\); l.Enabled\(\) { ... l.Info ... }` + if l.V(1).Enabled() { // want `the result of l.V should be stored in a variable and then be used multiple times: if l := l.V\(\); l.Enabled\(\) { ... l.Info ... }` l.V(1).Info("I'm logging at level 1.") } From 71791784b6c360f3fd8b707c842abb33e8bc389b Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 21 Mar 2022 13:16:25 +0100 Subject: [PATCH 072/125] hack/tools: drop dependency on golangci-lint Depending on the more recent golangci-lint 1.43.0 also pulled in a newer mock package, which caused problems when trying to use the logcheck plugin in Kubernetes because there were problems with generated mock files when using the newer mock. golangci-lint isn't actually needed (anymore?) for building the plugin, so a "go mod tidy" removed it. --- hack/tools/go.mod | 3 +- hack/tools/go.sum | 1136 +-------------------------------------------- 2 files changed, 4 insertions(+), 1135 deletions(-) diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 3b707b1c3..3c94929c9 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -3,7 +3,6 @@ module k8s.io/klog/hack/tools go 1.15 require ( - github.com/golangci/golangci-lint v1.43.0 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f - golang.org/x/tools v0.1.7 + golang.org/x/tools v0.1.0 ) diff --git a/hack/tools/go.sum b/hack/tools/go.sum index 8f9118e80..e89e59cf9 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -1,1176 +1,46 @@ -4d63.com/gochecknoglobals v0.1.0/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= -cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= -github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= -github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= -github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= -github.com/blizzy78/varnamelen v0.3.0/go.mod h1:hbwRdBvoBqxk34XyQ6HA0UH3G0/1TKuv5AC4eaBT0Ec= -github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= -github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= -github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= -github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= -github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= -github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.43.0 h1:SLwZFEmDgopqZpfP495zCtV9REUf551JJlJ51Ql7NZA= -github.com/golangci/golangci-lint v1.43.0/go.mod h1:VIFlUqidx5ggxDfQagdvd9E67UjMXtTHBkBQ7sHoC5Q= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= -github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw= -github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= -github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= -github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= -github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= -github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= -github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= -github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= -github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8BfaIeMzgBNKvc= -github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= -github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= -github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30= -github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ= -github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/dsl v0.3.10/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= -github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= -github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= -github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= -github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= -github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= -github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= -github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= -github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20210220032938-85be41e4509f h1:GrkO5AtFUU9U/1f5ctbIBXtBGeSJbWwIYfIsTcFMaX4= golang.org/x/exp v0.0.0-20210220032938-85be41e4509f/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= From 47f254f567e85d11b3f39982647d34466965804f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 21 Mar 2022 16:43:15 +0100 Subject: [PATCH 073/125] StopFlushDaemon: document flushing on shutdown This is how it was originally implemented and people may have started to rely on that now after inspecting the source code. --- klog.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/klog.go b/klog.go index cb04590fe..8f5871adb 100644 --- a/klog.go +++ b/klog.go @@ -1077,9 +1077,9 @@ func (f *flushDaemon) isRunning() bool { return f.stopC != nil } -// StopFlushDaemon stops the flush daemon, if running. +// StopFlushDaemon stops the flush daemon, if running, and flushes once. // This prevents klog from leaking goroutines on shutdown. After stopping -// the daemon, you can still manually flush buffers by calling Flush(). +// the daemon, you can still manually flush buffers again by calling Flush(). func StopFlushDaemon() { logging.flushD.stop() } From 50795822a0062fbdd03e99862bf7e4c195d8fdfb Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 23 Mar 2022 13:24:30 +0100 Subject: [PATCH 074/125] logcheck: fix detection of invalid * regexp in filter Extending the user-supplied regular expression to match the entire string by adding ^ and $ turned invalid regular expressions like * into valid ones (`^*$`) and thus didn't flag them as error. It's better to compile the string exactly as supplied by the user (better error message, properly detects this case) and then checking later whether the entire string was matched. --- hack/tools/logcheck/pkg/filter.go | 19 ++++++++++++++++--- hack/tools/logcheck/pkg/filter_test.go | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/hack/tools/logcheck/pkg/filter.go b/hack/tools/logcheck/pkg/filter.go index 231a38cb8..c2ad90925 100644 --- a/hack/tools/logcheck/pkg/filter.go +++ b/hack/tools/logcheck/pkg/filter.go @@ -88,8 +88,7 @@ func (f *RegexpFilter) Set(filename string) error { line.enabled[c] = enabled } - // Must match entire string. - re, err := regexp.Compile("^" + parts[1] + "$") + re, err := regexp.Compile(parts[1]) if err != nil { return fmt.Errorf("%s:%d: %v", filename, lineNr, err) } @@ -106,7 +105,8 @@ func (f *RegexpFilter) Set(filename string) error { // Enabled checks whether a certain check is enabled for a file. func (f *RegexpFilter) Enabled(check string, enabled bool, filename string) bool { for _, l := range f.lines { - if l.match.MatchString(filename) { + // Must match entire string. + if matchFullString(filename, l.match) { if e, ok := l.enabled[check]; ok { enabled = e } @@ -114,3 +114,16 @@ func (f *RegexpFilter) Enabled(check string, enabled bool, filename string) bool } return enabled } + +func matchFullString(str string, re *regexp.Regexp) bool { + loc := re.FindStringIndex(str) + if loc == nil { + // No match at all. + return false + } + if loc[1]-loc[0] < len(str) { + // Only matches a substring. + return false + } + return true +} diff --git a/hack/tools/logcheck/pkg/filter_test.go b/hack/tools/logcheck/pkg/filter_test.go index 115af49af..eee829a7b 100644 --- a/hack/tools/logcheck/pkg/filter_test.go +++ b/hack/tools/logcheck/pkg/filter_test.go @@ -116,7 +116,11 @@ func TestParsing(t *testing.T) { }{ "invalid-regexp": { content: `structured [`, - expectError: filename + ":0: error parsing regexp: missing closing ]: `[$`", + expectError: filename + ":0: error parsing regexp: missing closing ]: `[`", + }, + "wildcard": { + content: `structured *`, + expectError: filename + ":0: error parsing regexp: missing argument to repetition operator: `*`", }, "invalid-line": { content: `structured . From 2910cb140eda345a3f45f07cd22b059ead374762 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 4 Apr 2022 13:51:48 +0200 Subject: [PATCH 075/125] README.md: clarify -logtostderr It's a shortcut. The other options only matter when it is false. --- klog.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/klog.go b/klog.go index 8f5871adb..072e15979 100644 --- a/klog.go +++ b/klog.go @@ -41,6 +41,10 @@ // // -logtostderr=true // Logs are written to standard error instead of to files. +// This shortcuts most of the usual output routing: +// -alsologtostderr, -stderrthreshold and -log_dir have no +// effect and output redirection at runtime with SetOutput is +// ignored. // -alsologtostderr=false // Logs are written to standard error as well as to files. // -stderrthreshold=ERROR @@ -404,19 +408,19 @@ func InitFlags(flagset *flag.FlagSet) { flagset = flag.CommandLine } - flagset.StringVar(&logging.logDir, "log_dir", logging.logDir, "If non-empty, write log files in this directory") - flagset.StringVar(&logging.logFile, "log_file", logging.logFile, "If non-empty, use this log file") + flagset.StringVar(&logging.logDir, "log_dir", logging.logDir, "If non-empty, write log files in this directory (no effect when -logtostderr=true)") + flagset.StringVar(&logging.logFile, "log_file", logging.logFile, "If non-empty, use this log file (no effect when -logtostderr=true)") flagset.Uint64Var(&logging.logFileMaxSizeMB, "log_file_max_size", logging.logFileMaxSizeMB, - "Defines the maximum size a log file can grow to. Unit is megabytes. "+ + "Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. "+ "If the value is 0, the maximum file size is unlimited.") flagset.BoolVar(&logging.toStderr, "logtostderr", logging.toStderr, "log to standard error instead of files") - flagset.BoolVar(&logging.alsoToStderr, "alsologtostderr", logging.alsoToStderr, "log to standard error as well as files") + flagset.BoolVar(&logging.alsoToStderr, "alsologtostderr", logging.alsoToStderr, "log to standard error as well as files (no effect when -logtostderr=true)") flagset.Var(&logging.verbosity, "v", "number for the log level verbosity") flagset.BoolVar(&logging.addDirHeader, "add_dir_header", logging.addDirHeader, "If true, adds the file directory to the header of the log messages") flagset.BoolVar(&logging.skipHeaders, "skip_headers", logging.skipHeaders, "If true, avoid header prefixes in the log messages") - flagset.BoolVar(&logging.oneOutput, "one_output", logging.oneOutput, "If true, only write logs to their native severity level (vs also writing to each lower severity level)") - flagset.BoolVar(&logging.skipLogHeaders, "skip_log_headers", logging.skipLogHeaders, "If true, avoid headers when opening log files") - flagset.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr") + flagset.BoolVar(&logging.oneOutput, "one_output", logging.oneOutput, "If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true)") + flagset.BoolVar(&logging.skipLogHeaders, "skip_log_headers", logging.skipLogHeaders, "If true, avoid headers when opening log files (no effect when -logtostderr=true)") + flagset.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false)") flagset.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") flagset.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") } From ff92e06a340eb66c187a59f60562e273f2d90388 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 26 Apr 2022 13:16:02 +0200 Subject: [PATCH 076/125] test: add benchmark for KObjs This is a first step towards optimizing it. Performance is expected to depend primarily on the length of the slice, so that gets varied. --- examples/benchmarks/benchmarks_test.go | 152 +++++++++++++++++++++++++ klog_test.go | 25 +++- 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 examples/benchmarks/benchmarks_test.go diff --git a/examples/benchmarks/benchmarks_test.go b/examples/benchmarks/benchmarks_test.go new file mode 100644 index 000000000..eb8d5ae71 --- /dev/null +++ b/examples/benchmarks/benchmarks_test.go @@ -0,0 +1,152 @@ +/* +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 benchmarks + +import ( + "flag" + "fmt" + "io" + "testing" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "k8s.io/klog/v2" +) + +const ( + verbosityThreshold = 10 +) + +func init() { + // 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", fmt.Sprintf("%d", verbosityThreshold)) + flag.Set("log_file", "/dev/null") + flag.Set("logtostderr", "false") + flag.Set("alsologtostderr", "false") + flag.Set("stderrthreshold", "10") +} + +type testcase struct { + name string + generate func() interface{} +} + +func BenchmarkOutput(b *testing.B) { + // We'll run each benchmark for different output formatting. + configs := map[string]struct { + init, cleanup func() + }{ + "klog": { + init: func() { klog.SetOutput(discard{}) }, + }, + "zapr": { + init: func() { klog.SetLogger(newZaprLogger()) }, + cleanup: func() { klog.ClearLogger() }, + }, + } + + // Each benchmark tests formatting of one key/value pair, with + // different values. The order is relevant here. + var tests []testcase + for length := 0; length <= 100; length += 10 { + arg := make([]interface{}, length) + for i := 0; i < length; i++ { + arg[i] = KMetadataMock{Name: "a", NS: "a"} + } + tests = append(tests, testcase{ + name: fmt.Sprintf("objects/%d", length), + generate: func() interface{} { + return klog.KObjs(arg) + }, + }) + } + + // Verbosity checks may influence the result. + verbosity := map[string]func(value interface{}){ + "no-verbosity-check": func(value interface{}) { + klog.InfoS("test", "key", value) + }, + "pass-verbosity-check": func(value interface{}) { + klog.V(verbosityThreshold).InfoS("test", "key", value) + }, + "fail-verbosity-check": func(value interface{}) { + klog.V(verbosityThreshold+1).InfoS("test", "key", value) + }, + } + + for name, config := range configs { + b.Run(name, func(b *testing.B) { + if config.cleanup != nil { + defer config.cleanup() + } + config.init() + + for name, logCall := range verbosity { + b.Run(name, func(b *testing.B) { + for _, testcase := range tests { + b.Run(testcase.name, func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + logCall(testcase.generate()) + } + }) + } + }) + } + }) + } +} + +func newZaprLogger() logr.Logger { + encoderConfig := &zapcore.EncoderConfig{ + MessageKey: "msg", + CallerKey: "caller", + NameKey: "logger", + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } + encoder := zapcore.NewJSONEncoder(*encoderConfig) + zapV := -zapcore.Level(verbosityThreshold) + core := zapcore.NewCore(encoder, zapcore.AddSync(discard{}), zapV) + l := zap.New(core, zap.WithCaller(true)) + logger := zapr.NewLoggerWithOptions(l, zapr.LogInfoLevel("v"), zapr.ErrorKey("err")) + return logger +} + +type KMetadataMock struct { + Name, NS string +} + +func (m KMetadataMock) GetName() string { + return m.Name +} +func (m KMetadataMock) GetNamespace() string { + return m.NS +} + +type discard struct{} + +var _ io.Writer = discard{} + +func (discard) Write(p []byte) (int, error) { + return len(p), nil +} diff --git a/klog_test.go b/klog_test.go index a23f5716f..3a9b4494b 100644 --- a/klog_test.go +++ b/klog_test.go @@ -631,7 +631,7 @@ func BenchmarkHeaderWithDir(b *testing.B) { } // Ensure that benchmarks have side effects to avoid compiler optimization -var result ObjectRef +var result interface{} var enabled bool func BenchmarkV(b *testing.B) { @@ -659,6 +659,29 @@ func BenchmarkKObj(b *testing.B) { result = r } +// BenchmarkKObjs measures the (pretty typical) case +// where KObjs is used in a V(5).InfoS call that never +// emits a log entry because verbosity is lower than 5. +// For performance when the result of KObjs gets formatted, +// see examples/benchmarks. +func BenchmarkKObjs(b *testing.B) { + for length := 0; length <= 100; length += 10 { + b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { + arg := make([]interface{}, length) + for i := 0; i < length; i++ { + arg[i] = test.KMetadataMock{Name: "a", NS: "a"} + } + b.ResetTimer() + + var r interface{} + for i := 0; i < b.N; i++ { + r = KObjs(arg) + } + result = r + }) + } +} + func BenchmarkLogs(b *testing.B) { setFlags() defer logging.swap(logging.newBuffers()) From 296f5e641c87b3dfe70dcb6616ac45aad87e3557 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 6 Apr 2022 08:48:41 +0200 Subject: [PATCH 077/125] save and restore state This is useful in several unit tests. At least one test in klog_test.go itself left klog in a different state after it ran. --- klog.go | 116 ++++++++++++++++++++++++++++++++++++++--- klog_test.go | 142 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 227 insertions(+), 31 deletions(-) diff --git a/klog.go b/klog.go index 8f5871adb..f5650ffc8 100644 --- a/klog.go +++ b/klog.go @@ -242,6 +242,10 @@ func (m *moduleSpec) String() string { // Lock because the type is not atomic. TODO: clean this up. logging.mu.Lock() defer logging.mu.Unlock() + return m.serialize() +} + +func (m *moduleSpec) serialize() string { var b bytes.Buffer for i, f := range m.filter { if i > 0 { @@ -263,6 +267,17 @@ var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of // Set will sets module value // Syntax: -vmodule=recordio=2,file=1,gfs*=3 func (m *moduleSpec) Set(value string) error { + filter, err := parseModuleSpec(value) + if err != nil { + return err + } + logging.mu.Lock() + defer logging.mu.Unlock() + logging.setVState(logging.verbosity, filter, true) + return nil +} + +func parseModuleSpec(value string) ([]modulePat, error) { var filter []modulePat for _, pat := range strings.Split(value, ",") { if len(pat) == 0 { @@ -271,15 +286,15 @@ func (m *moduleSpec) Set(value string) error { } patLev := strings.Split(pat, "=") if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 { - return errVmoduleSyntax + return nil, 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") + return nil, errors.New("syntax error: expect comma-separated list of filename=N") } if v < 0 { - return errors.New("negative value for vmodule level") + return nil, errors.New("negative value for vmodule level") } if v == 0 { continue // Ignore. It's harmless but no point in paying the overhead. @@ -287,10 +302,7 @@ func (m *moduleSpec) Set(value string) error { // TODO: check syntax of filter? filter = append(filter, modulePat{pattern, isLiteral(pattern), Level(v)}) } - logging.mu.Lock() - defer logging.mu.Unlock() - logging.setVState(logging.verbosity, filter, true) - return nil + return filter, nil } // isLiteral reports whether the pattern is a literal string, that is, has no metacharacters @@ -520,6 +532,96 @@ func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool var timeNow = time.Now // Stubbed out for testing. +// CaptureState gathers information about all current klog settings. +// The result can be used to restore those settings. +func CaptureState() State { + logging.mu.Lock() + defer logging.mu.Unlock() + return &state{ + // We cannot simply copy the entire struct because + // it contains a mutex which cannot be copied. + loggingT: loggingT{ + toStderr: logging.toStderr, + alsoToStderr: logging.alsoToStderr, + stderrThreshold: logging.stderrThreshold, + file: logging.file, + flushInterval: logging.flushInterval, + traceLocation: logging.traceLocation, + verbosity: logging.verbosity, + logDir: logging.logDir, + logFile: logging.logFile, + logFileMaxSizeMB: logging.logFileMaxSizeMB, + skipHeaders: logging.skipHeaders, + skipLogHeaders: logging.skipLogHeaders, + addDirHeader: logging.addDirHeader, + oneOutput: logging.oneOutput, + filter: logging.filter, + }, + logger: globalLogger, + flushDRunning: logging.flushD.isRunning(), + moduleSpec: logging.vmodule.serialize(), + loggerOptions: globalLoggerOptions, + maxSize: MaxSize, + } +} + +// State stores a snapshot of klog settings. It gets created with CaptureState +// and can be used to restore the entire state. Modifying individual settings +// is supported via the command line flags. +type State interface { + // Restore restore the entire state. It may get called more than once. + Restore() +} + +type state struct { + loggingT + + logger *Logger + loggerOptions loggerOptions + flushDRunning bool + moduleSpec string + maxSize uint64 +} + +func (s *state) Restore() { + // This needs to be done before mutex locking. + if s.flushDRunning && !logging.flushD.isRunning() { + // This is not quite accurate: StartFlushDaemon might + // have been called with some different interval. + interval := s.flushInterval + if interval == 0 { + interval = flushInterval + } + logging.flushD.run(interval) + } else if !s.flushDRunning && logging.flushD.isRunning() { + logging.flushD.stop() + } + + logging.mu.Lock() + defer logging.mu.Unlock() + + logging.toStderr = s.toStderr + logging.alsoToStderr = s.alsoToStderr + logging.stderrThreshold = s.stderrThreshold + logging.file = s.file + logging.flushInterval = s.flushInterval + logging.traceLocation = s.traceLocation + logging.verbosity = s.verbosity + logging.logDir = s.logDir + logging.logFile = s.logFile + logging.logFileMaxSizeMB = s.logFileMaxSizeMB + logging.skipHeaders = s.skipHeaders + logging.skipLogHeaders = s.skipLogHeaders + logging.addDirHeader = s.addDirHeader + logging.oneOutput = s.oneOutput + logging.filter = s.filter + filter, _ := parseModuleSpec(s.moduleSpec) + logging.setVState(s.verbosity, filter, true) + globalLogger = s.logger + globalLoggerOptions = s.loggerOptions + MaxSize = s.maxSize +} + /* header formats a log header as defined by the C++ implementation. It returns a buffer containing the formatted header and the user's file and line number. diff --git a/klog_test.go b/klog_test.go index a23f5716f..5ecb9802e 100644 --- a/klog_test.go +++ b/klog_test.go @@ -105,6 +105,7 @@ func setFlags() { // Test that Info works as advertised. func TestInfo(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) Info("test") @@ -117,6 +118,7 @@ func TestInfo(t *testing.T) { } func TestInfoDepth(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) @@ -176,6 +178,7 @@ func TestCopyStandardLogToPanic(t *testing.T) { // Test that using the standard log package logs to INFO. func TestStandardLog(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) stdLog.Print("test") @@ -189,6 +192,7 @@ func TestStandardLog(t *testing.T) { // Test that the header has the correct format. func TestHeader(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) defer func(previous func() time.Time) { timeNow = previous }(timeNow) @@ -212,6 +216,7 @@ func TestHeader(t *testing.T) { } func TestHeaderWithDir(t *testing.T) { + defer CaptureState().Restore() setFlags() logging.addDirHeader = true defer logging.swap(logging.newBuffers()) @@ -231,6 +236,7 @@ func TestHeaderWithDir(t *testing.T) { // Even in the Info log, the source character will be E, so the data should // all be identical. func TestError(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) Error("test") @@ -253,13 +259,10 @@ func TestError(t *testing.T) { // Even in the Info log, the source character will be E, so the data should // all be identical. func TestErrorWithOneOutput(t *testing.T) { + defer CaptureState().Restore() setFlags() logging.oneOutput = true - buf := logging.newBuffers() - defer func() { - logging.swap(buf) - logging.oneOutput = false - }() + defer logging.swap(logging.newBuffers()) Error("test") if !contains(severity.ErrorLog, "E", t) { t.Errorf("Error has wrong character: %q", contents(severity.ErrorLog)) @@ -280,6 +283,7 @@ func TestErrorWithOneOutput(t *testing.T) { // Even in the Info log, the source character will be W, so the data should // all be identical. func TestWarning(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) Warning("test") @@ -299,13 +303,10 @@ func TestWarning(t *testing.T) { // Even in the Info log, the source character will be W, so the data should // all be identical. func TestWarningWithOneOutput(t *testing.T) { + defer CaptureState().Restore() setFlags() logging.oneOutput = true - buf := logging.newBuffers() - defer func() { - logging.swap(buf) - logging.oneOutput = false - }() + defer logging.swap(logging.newBuffers()) Warning("test") if !contains(severity.WarningLog, "W", t) { t.Errorf("Warning has wrong character: %q", contents(severity.WarningLog)) @@ -321,10 +322,10 @@ func TestWarningWithOneOutput(t *testing.T) { // Test that a V log goes to Info. func TestV(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) logging.verbosity.Set("2") - defer logging.verbosity.Set("0") V(2).Info("test") if !contains(severity.InfoLog, "I", t) { t.Errorf("Info has wrong character: %q", contents(severity.InfoLog)) @@ -336,10 +337,10 @@ func TestV(t *testing.T) { // Test that a vmodule enables a log in this file. func TestVmoduleOn(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) logging.vmodule.Set("klog_test=2") - defer logging.vmodule.Set("") if !V(1).Enabled() { t.Error("V not enabled for 1") } @@ -360,10 +361,10 @@ func TestVmoduleOn(t *testing.T) { // Test that a vmodule of another file does not enable a log in this file. func TestVmoduleOff(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) logging.vmodule.Set("notthisfile=2") - defer logging.vmodule.Set("") for i := 1; i <= 3; i++ { if V(Level(i)).Enabled() { t.Errorf("V enabled for %d", i) @@ -376,6 +377,7 @@ func TestVmoduleOff(t *testing.T) { } func TestSetOutputDataRace(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) var wg sync.WaitGroup @@ -416,6 +418,7 @@ func TestSetOutputDataRace(t *testing.T) { } func TestLogToOutput(t *testing.T) { + defer CaptureState().Restore() logging.toStderr = true defer logging.swap(logging.newBuffers()) buf := new(bytes.Buffer) @@ -450,9 +453,9 @@ var vGlobs = map[string]bool{ // Test that vmodule globbing works as advertised. func testVmoduleGlob(pat string, match bool, t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) - defer logging.vmodule.Set("") logging.vmodule.Set(pat) if V(2).Enabled() != match { t.Errorf("incorrect match for %q: got %#v expected %#v", pat, V(2), match) @@ -467,13 +470,13 @@ func TestVmoduleGlob(t *testing.T) { } func TestRollover(t *testing.T) { + defer CaptureState().Restore() setFlags() var err error defer func(previous func(error)) { logExitFunc = previous }(logExitFunc) logExitFunc = func(e error) { err = e } - defer func(previous uint64) { MaxSize = previous }(MaxSize) MaxSize = 512 Info("x") // Be sure we have a file. info, ok := logging.file[severity.InfoLog].(*syncBuffer) @@ -516,6 +519,7 @@ func TestOpenAppendOnStart(t *testing.T) { y string = "yyyyyyyyyy" ) + defer CaptureState().Restore() setFlags() var err error defer func(previous func(error)) { logExitFunc = previous }(logExitFunc) @@ -580,6 +584,7 @@ func TestOpenAppendOnStart(t *testing.T) { } func TestLogBacktraceAt(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) // The peculiar style of this code simplifies line counting and maintenance of the @@ -660,6 +665,7 @@ func BenchmarkKObj(b *testing.B) { } func BenchmarkLogs(b *testing.B) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) @@ -688,6 +694,7 @@ func BenchmarkLogs(b *testing.B) { // Test the logic on checking log size limitation. func TestFileSizeCheck(t *testing.T) { + defer CaptureState().Restore() setFlags() testData := map[string]struct { testLogFile string @@ -750,6 +757,7 @@ func TestInitFlags(t *testing.T) { } func TestInfoObjectRef(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) @@ -879,6 +887,7 @@ func TestKRef(t *testing.T) { // Test that InfoS and InfoSDepth work as advertised. func TestInfoS(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) timeNow = func() time.Time { @@ -935,6 +944,7 @@ func TestInfoS(t *testing.T) { // Test that Verbose.InfoS works as advertised. func TestVInfoS(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) timeNow = func() time.Time { @@ -990,7 +1000,6 @@ second value line`}, } logging.verbosity.Set("2") - defer logging.verbosity.Set("0") for l := Level(0); l < Level(4); l++ { for _, data := range testDataInfo { @@ -1019,6 +1028,7 @@ second value line`}, // Test that ErrorS and ErrorSDepth work as advertised. func TestErrorS(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) timeNow = func() time.Time { @@ -1147,10 +1157,10 @@ func (f *sampleLogFilter) FilterS(msg string, keysAndValues []interface{}) (stri } func TestLogFilter(t *testing.T) { + defer CaptureState().Restore() setFlags() defer logging.swap(logging.newBuffers()) SetLogFilter(&sampleLogFilter{}) - defer SetLogFilter(nil) funcs := []struct { name string logFunc func(args ...interface{}) @@ -1316,8 +1326,8 @@ func TestInfoWithLogr(t *testing.T) { for _, data := range testDataInfo { t.Run(data.msg, func(t *testing.T) { l := logr.New(logger) + defer CaptureState().Restore() SetLogger(l) - defer ClearLogger() defer logger.reset() Info(data.msg) @@ -1356,9 +1366,9 @@ func TestInfoSWithLogr(t *testing.T) { for _, data := range testDataInfo { t.Run(data.msg, func(t *testing.T) { + defer CaptureState().Restore() l := logr.New(logger) SetLogger(l) - defer ClearLogger() defer logger.reset() InfoS(data.msg, data.keysValues...) @@ -1424,9 +1434,9 @@ func TestErrorSWithLogr(t *testing.T) { for _, data := range testDataInfo { t.Run(data.msg, func(t *testing.T) { + defer CaptureState().Restore() l := logr.New(logger) SetLogger(l) - defer ClearLogger() defer logger.reset() ErrorS(data.err, data.msg, data.keysValues...) @@ -1483,8 +1493,8 @@ func TestCallDepthLogr(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { l := logr.New(logger) - SetLogger(l) defer ClearLogger() + SetLogger(l) defer logger.reset() defer logger.resetCallDepth() @@ -1505,8 +1515,8 @@ func TestCallDepthLogrInfoS(t *testing.T) { logger := &callDepthTestLogr{} logger.resetCallDepth() l := logr.New(logger) + defer CaptureState().Restore() SetLogger(l) - defer ClearLogger() // Add wrapper to ensure callDepthTestLogr +2 offset is correct. logFunc := func() { @@ -1528,8 +1538,8 @@ func TestCallDepthLogrErrorS(t *testing.T) { logger := &callDepthTestLogr{} logger.resetCallDepth() l := logr.New(logger) + defer CaptureState().Restore() SetLogger(l) - defer ClearLogger() // Add wrapper to ensure callDepthTestLogr +2 offset is correct. logFunc := func() { @@ -1548,11 +1558,11 @@ func TestCallDepthLogrErrorS(t *testing.T) { } func TestCallDepthLogrGoLog(t *testing.T) { + defer CaptureState().Restore() logger := &callDepthTestLogr{} logger.resetCallDepth() l := logr.New(logger) SetLogger(l) - defer ClearLogger() CopyStandardLogTo("INFO") // Add wrapper to ensure callDepthTestLogr +2 offset is correct. @@ -1921,3 +1931,87 @@ func TestStopFlushDaemon(t *testing.T) { t.Error("expected flushD to be stopped") } } + +func TestCaptureState(t *testing.T) { + var fs flag.FlagSet + InitFlags(&fs) + + // Capture state. + oldState := map[string]string{} + fs.VisitAll(func(f *flag.Flag) { + oldState[f.Name] = f.Value.String() + }) + originalLogger := Background() + file := logging.file + + // And through dedicated API. + // Ensure we always restore. + state := CaptureState() + defer state.Restore() + + // Change state. + for name, value := range map[string]string{ + // All of these are non-standard values. + "v": "10", + "vmodule": "abc=2", + "log_dir": "/tmp", + "log_file_max_size": "10", + "logtostderr": "false", + "alsologtostderr": "true", + "add_dir_header": "true", + "skip_headers": "true", + "one_output": "true", + "skip_log_headers": "true", + "stderrthreshold": "1", + "log_backtrace_at": "foobar.go:100", + } { + f := fs.Lookup(name) + if f == nil { + t.Fatalf("could not look up %q", name) + } + currentValue := f.Value.String() + if currentValue == value { + t.Fatalf("%q is already set to non-default %q?!", name, value) + } + if err := f.Value.Set(value); err != nil { + t.Fatalf("setting %q to %q: %v", name, value, err) + } + } + StartFlushDaemon(time.Minute) + if !logging.flushD.isRunning() { + t.Error("Flush daemon should have been started.") + } + logger := logr.Discard() + SetLoggerWithOptions(logger, ContextualLogger(true)) + actualLogger := Background() + if logger != actualLogger { + t.Errorf("Background logger should be %v, got %v", logger, actualLogger) + } + buffer := bytes.Buffer{} + SetOutput(&buffer) + if file == logging.file { + t.Error("Output files should have been modified.") + } + + // Let klog restore the state. + state.Restore() + + // Verify that the original state is back. + fs.VisitAll(func(f *flag.Flag) { + oldValue := oldState[f.Name] + currentValue := f.Value.String() + if oldValue != currentValue { + t.Errorf("%q should have been restored to %q, is %q instead", f.Name, oldValue, currentValue) + } + }) + if logging.flushD.isRunning() { + t.Error("Flush daemon should have been stopped.") + } + actualLogger = Background() + if originalLogger != actualLogger { + t.Errorf("Background logger should be %v, got %v", originalLogger, actualLogger) + } + if file != logging.file { + t.Errorf("Output files should have been restored to %v, got %v", file, logging.file) + } +} From 3c90bf9a79bf6ed7c82fa8cafeaf1681c8555255 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 9 May 2022 12:30:36 +0200 Subject: [PATCH 078/125] refactor global state Global state consists of user-controlled settings and some additional runtime variables (mutex, cache). Storing all settings, and just those, in a single struct with a deepCopy function makes state capturing simpler. --- contextual.go | 38 ++++------ klog.go | 200 +++++++++++++++++++++++++------------------------- klog_test.go | 29 +++++++- 3 files changed, 138 insertions(+), 129 deletions(-) diff --git a/contextual.go b/contextual.go index 0bf19280e..65ac479ab 100644 --- a/contextual.go +++ b/contextual.go @@ -34,18 +34,6 @@ import ( // mutex locking. var ( - // contextualLoggingEnabled controls whether contextual logging is - // active. Disabling it may have some small performance benefit. - contextualLoggingEnabled = true - - // globalLogger is the global Logger chosen by users of klog, nil if - // none is available. - globalLogger *Logger - - // globalLoggerOptions contains the options that were supplied for - // globalLogger. - globalLoggerOptions loggerOptions - // klogLogger is used as fallback for logging through the normal klog code // when no Logger is set. klogLogger logr.Logger = logr.New(&klogger{}) @@ -81,10 +69,10 @@ func SetLogger(logger logr.Logger) { // routing log entries through klogr into klog and then into the actual Logger // backend. func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { - globalLogger = &logger - globalLoggerOptions = loggerOptions{} + logging.logger = &logger + logging.loggerOptions = loggerOptions{} for _, opt := range opts { - opt(&globalLoggerOptions) + opt(&logging.loggerOptions) } } @@ -119,8 +107,8 @@ type loggerOptions struct { // Modifying the logger is not thread-safe and should be done while no other // goroutines invoke log calls, usually during program initialization. func ClearLogger() { - globalLogger = nil - globalLoggerOptions = loggerOptions{} + logging.logger = nil + logging.loggerOptions = loggerOptions{} } // EnableContextualLogging controls whether contextual logging is enabled. @@ -132,14 +120,14 @@ func ClearLogger() { // // This must be called during initialization before goroutines are started. func EnableContextualLogging(enabled bool) { - contextualLoggingEnabled = enabled + logging.contextualLoggingEnabled = enabled } // FromContext retrieves a logger set by the caller or, if not set, // falls back to the program's global logger (a Logger instance or klog // itself). func FromContext(ctx context.Context) Logger { - if contextualLoggingEnabled { + if logging.contextualLoggingEnabled { if logger, err := logr.FromContext(ctx); err == nil { return logger } @@ -160,10 +148,10 @@ func TODO() Logger { // better receive a logger via its parameters. TODO can be used as a temporary // solution for such code. func Background() Logger { - if globalLoggerOptions.contextualLogger { - // Is non-nil because globalLoggerOptions.contextualLogger is + if logging.loggerOptions.contextualLogger { + // Is non-nil because logging.loggerOptions.contextualLogger is // only true if a logger was set. - return *globalLogger + return *logging.logger } return klogLogger @@ -172,7 +160,7 @@ func Background() Logger { // LoggerWithValues returns logger.WithValues(...kv) when // contextual logging is enabled, otherwise the logger. func LoggerWithValues(logger Logger, kv ...interface{}) Logger { - if contextualLoggingEnabled { + if logging.contextualLoggingEnabled { return logger.WithValues(kv...) } return logger @@ -181,7 +169,7 @@ func LoggerWithValues(logger Logger, kv ...interface{}) Logger { // LoggerWithName returns logger.WithName(name) when contextual logging is // enabled, otherwise the logger. func LoggerWithName(logger Logger, name string) Logger { - if contextualLoggingEnabled { + if logging.contextualLoggingEnabled { return logger.WithName(name) } return logger @@ -190,7 +178,7 @@ func LoggerWithName(logger Logger, name string) Logger { // NewContext returns logr.NewContext(ctx, logger) when // contextual logging is enabled, otherwise ctx. func NewContext(ctx context.Context, logger Logger) context.Context { - if contextualLoggingEnabled { + if logging.contextualLoggingEnabled { return logr.NewContext(ctx, logger) } return ctx diff --git a/klog.go b/klog.go index f5650ffc8..ef7a48ab1 100644 --- a/klog.go +++ b/klog.go @@ -438,8 +438,20 @@ func Flush() { logging.lockAndFlushAll() } -// loggingT collects all the global state of the logging setup. -type loggingT struct { +// settings collects global settings. +type settings struct { + // contextualLoggingEnabled controls whether contextual logging is + // active. Disabling it may have some small performance benefit. + contextualLoggingEnabled bool + + // logger is the global Logger chosen by users of klog, nil if + // none is available. + logger *Logger + + // loggerOptions contains the options that were supplied for + // globalLogger. + loggerOptions loggerOptions + // Boolean flags. Not handled atomically because the flag.Value interface // does not let us avoid the =true, and that shorthand is necessary for // compatibility. TODO: does this matter enough to fix? Seems unlikely. @@ -449,26 +461,14 @@ type loggingT struct { // Level flag. Handled atomically. stderrThreshold severityValue // The -stderrthreshold flag. - // 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. - bufferCache buffer.Buffers + // Access to all of the following fields must be protected via a mutex. - // 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 [severity.NumSeverity]flushSyncWriter - // flushD holds a flushDaemon that frequently flushes log file buffers. - flushD *flushDaemon // flushInterval is the interval for periodic flushing. If zero, // the global default will be used. flushInterval time.Duration - // 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. @@ -508,6 +508,43 @@ type loggingT struct { filter LogFilter } +// deepCopy creates a copy that doesn't share anything with the original +// instance. +func (s settings) deepCopy() settings { + // vmodule is a slice and would be shared, so we have copy it. + filter := make([]modulePat, len(s.vmodule.filter)) + for i := range s.vmodule.filter { + filter[i] = s.vmodule.filter[i] + } + s.vmodule.filter = filter + + return s +} + +// loggingT collects all the global state of the logging setup. +type loggingT struct { + settings + + // 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. + bufferCache buffer.Buffers + + // flushD holds a flushDaemon that frequently flushes log file buffers. + // Uses its own mutex. + flushD *flushDaemon + + // mu protects the remaining elements of this structure and the fields + // in settingsT which need a mutex lock. + mu sync.Mutex + + // 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 +} + var logging loggingT // setVState sets a consistent state for V logging. @@ -538,29 +575,8 @@ func CaptureState() State { logging.mu.Lock() defer logging.mu.Unlock() return &state{ - // We cannot simply copy the entire struct because - // it contains a mutex which cannot be copied. - loggingT: loggingT{ - toStderr: logging.toStderr, - alsoToStderr: logging.alsoToStderr, - stderrThreshold: logging.stderrThreshold, - file: logging.file, - flushInterval: logging.flushInterval, - traceLocation: logging.traceLocation, - verbosity: logging.verbosity, - logDir: logging.logDir, - logFile: logging.logFile, - logFileMaxSizeMB: logging.logFileMaxSizeMB, - skipHeaders: logging.skipHeaders, - skipLogHeaders: logging.skipLogHeaders, - addDirHeader: logging.addDirHeader, - oneOutput: logging.oneOutput, - filter: logging.filter, - }, - logger: globalLogger, + settings: logging.settings.deepCopy(), flushDRunning: logging.flushD.isRunning(), - moduleSpec: logging.vmodule.serialize(), - loggerOptions: globalLoggerOptions, maxSize: MaxSize, } } @@ -574,12 +590,9 @@ type State interface { } type state struct { - loggingT + settings - logger *Logger - loggerOptions loggerOptions flushDRunning bool - moduleSpec string maxSize uint64 } @@ -600,25 +613,8 @@ func (s *state) Restore() { logging.mu.Lock() defer logging.mu.Unlock() - logging.toStderr = s.toStderr - logging.alsoToStderr = s.alsoToStderr - logging.stderrThreshold = s.stderrThreshold - logging.file = s.file - logging.flushInterval = s.flushInterval - logging.traceLocation = s.traceLocation - logging.verbosity = s.verbosity - logging.logDir = s.logDir - logging.logFile = s.logFile - logging.logFileMaxSizeMB = s.logFileMaxSizeMB - logging.skipHeaders = s.skipHeaders - logging.skipLogHeaders = s.skipLogHeaders - logging.addDirHeader = s.addDirHeader - logging.oneOutput = s.oneOutput - logging.filter = s.filter - filter, _ := parseModuleSpec(s.moduleSpec) - logging.setVState(s.verbosity, filter, true) - globalLogger = s.logger - globalLoggerOptions = s.loggerOptions + logging.settings = s.settings + logging.setVState(s.verbosity, s.vmodule.filter, true) MaxSize = s.maxSize } @@ -790,7 +786,7 @@ func (l *loggingT) printS(err error, s severity.Severity, depth int, msg string, serialize.KVListFormat(&b.Buffer, "err", err) } serialize.KVListFormat(&b.Buffer, keysAndValues...) - l.printDepth(s, globalLogger, nil, depth+1, &b.Buffer) + l.printDepth(s, logging.logger, nil, depth+1, &b.Buffer) // Make the buffer available for reuse. l.bufferCache.PutBuffer(b) } @@ -867,7 +863,7 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf // TODO: set 'severity' and caller information as structured log info // keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line} if s == severity.ErrorLog { - globalLogger.WithCallDepth(depth+3).Error(nil, string(data)) + logging.logger.WithCallDepth(depth+3).Error(nil, string(data)) } else { log.WithCallDepth(depth + 3).Info(string(data)) } @@ -1211,8 +1207,8 @@ func (l *loggingT) flushAll() { file.Sync() // ignore error } } - if globalLoggerOptions.flush != nil { - globalLoggerOptions.flush() + if logging.loggerOptions.flush != nil { + logging.loggerOptions.flush() } } @@ -1260,7 +1256,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.Severity(lb), globalLogger, logging.filter, file, line, true, text) + logging.printWithFileLine(severity.Severity(lb), logging.logger, logging.filter, file, line, true, text) return len(b), nil } @@ -1298,10 +1294,10 @@ type Verbose struct { } func newVerbose(level Level, b bool) Verbose { - if globalLogger == nil { + if logging.logger == nil { return Verbose{b, nil} } - v := globalLogger.V(int(level)) + v := logging.logger.V(int(level)) return Verbose{b, &v} } @@ -1420,7 +1416,7 @@ func (v Verbose) InfoS(msg string, keysAndValues ...interface{}) { // InfoSDepth acts as InfoS but uses depth to determine which call frame to log. // InfoSDepth(0, "msg") is the same as InfoS("msg"). func InfoSDepth(depth int, msg string, keysAndValues ...interface{}) { - logging.infoS(globalLogger, logging.filter, depth, msg, keysAndValues...) + logging.infoS(logging.logger, logging.filter, depth, msg, keysAndValues...) } // InfoSDepth is equivalent to the global InfoSDepth function, guarded by the value of v. @@ -1449,37 +1445,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(severity.InfoLog, globalLogger, logging.filter, args...) + logging.print(severity.InfoLog, logging.logger, 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(severity.InfoLog, globalLogger, logging.filter, depth, args...) + logging.printDepth(severity.InfoLog, logging.logger, 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(severity.InfoLog, globalLogger, logging.filter, args...) + logging.println(severity.InfoLog, logging.logger, 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(severity.InfoLog, globalLogger, logging.filter, depth, args...) + logging.printlnDepth(severity.InfoLog, logging.logger, 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(severity.InfoLog, globalLogger, logging.filter, format, args...) + logging.printf(severity.InfoLog, logging.logger, 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(severity.InfoLog, globalLogger, logging.filter, depth, format, args...) + logging.printfDepth(severity.InfoLog, logging.logger, logging.filter, depth, format, args...) } // InfoS structured logs to the INFO log. @@ -1491,79 +1487,79 @@ func InfofDepth(depth int, format string, args ...interface{}) { // output: // >> I1025 00:15:15.525108 1 controller_utils.go:116] "Pod status updated" pod="kubedns" status="ready" func InfoS(msg string, keysAndValues ...interface{}) { - logging.infoS(globalLogger, logging.filter, 0, msg, keysAndValues...) + logging.infoS(logging.logger, logging.filter, 0, msg, keysAndValues...) } // Warning logs to the WARNING and INFO logs. // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Warning(args ...interface{}) { - logging.print(severity.WarningLog, globalLogger, logging.filter, args...) + logging.print(severity.WarningLog, logging.logger, 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(severity.WarningLog, globalLogger, logging.filter, depth, args...) + logging.printDepth(severity.WarningLog, logging.logger, 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(severity.WarningLog, globalLogger, logging.filter, args...) + logging.println(severity.WarningLog, logging.logger, 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(severity.WarningLog, globalLogger, logging.filter, depth, args...) + logging.printlnDepth(severity.WarningLog, logging.logger, 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(severity.WarningLog, globalLogger, logging.filter, format, args...) + logging.printf(severity.WarningLog, logging.logger, 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(severity.WarningLog, globalLogger, logging.filter, depth, format, args...) + logging.printfDepth(severity.WarningLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, args...) + logging.print(severity.ErrorLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, depth, args...) + logging.printDepth(severity.ErrorLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, args...) + logging.println(severity.ErrorLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, depth, args...) + logging.printlnDepth(severity.ErrorLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, format, args...) + logging.printf(severity.ErrorLog, logging.logger, 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(severity.ErrorLog, globalLogger, logging.filter, depth, format, args...) + logging.printfDepth(severity.ErrorLog, logging.logger, logging.filter, depth, format, args...) } // ErrorS structured logs to the ERROR, WARNING, and INFO logs. @@ -1576,52 +1572,52 @@ func ErrorfDepth(depth int, format string, args ...interface{}) { // output: // >> E1025 00:15:15.525108 1 controller_utils.go:114] "Failed to update pod status" err="timeout" func ErrorS(err error, msg string, keysAndValues ...interface{}) { - logging.errorS(err, globalLogger, logging.filter, 0, msg, keysAndValues...) + logging.errorS(err, logging.logger, logging.filter, 0, msg, keysAndValues...) } // ErrorSDepth acts as ErrorS but uses depth to determine which call frame to log. // ErrorSDepth(0, "msg") is the same as ErrorS("msg"). func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) { - logging.errorS(err, globalLogger, logging.filter, depth, msg, keysAndValues...) + logging.errorS(err, logging.logger, logging.filter, depth, msg, keysAndValues...) } // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, // including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Fatal(args ...interface{}) { - logging.print(severity.FatalLog, globalLogger, logging.filter, args...) + logging.print(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, args...) + logging.printDepth(severity.FatalLog, logging.logger, logging.filter, depth, args...) } // Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs, // including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Println; a newline is always appended. func Fatalln(args ...interface{}) { - logging.println(severity.FatalLog, globalLogger, logging.filter, args...) + logging.println(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, args...) + logging.printlnDepth(severity.FatalLog, logging.logger, logging.filter, depth, args...) } // Fatalf logs to the FATAL, ERROR, WARNING, and INFO logs, // including a stack trace of all running goroutines, then calls OsExit(255). // Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. func Fatalf(format string, args ...interface{}) { - logging.printf(severity.FatalLog, globalLogger, logging.filter, format, args...) + logging.printf(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, format, args...) + logging.printfDepth(severity.FatalLog, logging.logger, logging.filter, depth, format, args...) } // fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks. @@ -1632,41 +1628,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(severity.FatalLog, globalLogger, logging.filter, args...) + logging.print(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, args...) + logging.printDepth(severity.FatalLog, logging.logger, logging.filter, depth, args...) } // Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls OsExit(1). func Exitln(args ...interface{}) { atomic.StoreUint32(&fatalNoStacks, 1) - logging.println(severity.FatalLog, globalLogger, logging.filter, args...) + logging.println(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, args...) + logging.printlnDepth(severity.FatalLog, logging.logger, logging.filter, depth, args...) } // Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls OsExit(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(severity.FatalLog, globalLogger, logging.filter, format, args...) + logging.printf(severity.FatalLog, logging.logger, 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(severity.FatalLog, globalLogger, logging.filter, depth, format, args...) + logging.printfDepth(severity.FatalLog, logging.logger, 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 5ecb9802e..177050dc1 100644 --- a/klog_test.go +++ b/klog_test.go @@ -1879,8 +1879,10 @@ func TestFlushDaemon(t *testing.T) { } testClock := testingclock.NewFakeClock(time.Now()) testLog := loggingT{ - flushInterval: time.Second, - flushD: newFlushDaemon(spyFunc, testClock), + settings: settings{ + flushInterval: time.Second, + }, + flushD: newFlushDaemon(spyFunc, testClock), } // Calling testLog will call createFile, which should start the daemon. @@ -2015,3 +2017,26 @@ func TestCaptureState(t *testing.T) { t.Errorf("Output files should have been restored to %v, got %v", file, logging.file) } } + +func TestSettingsDeepCopy(t *testing.T) { + logger := logr.Discard() + + settings := settings{ + logger: &logger, + vmodule: moduleSpec{ + filter: []modulePat{ + {pattern: "a"}, + {pattern: "b"}, + {pattern: "c"}, + }, + }, + } + copy := settings.deepCopy() + if !reflect.DeepEqual(settings, copy) { + t.Fatalf("Copy not identical to original settings. Original:\n %+v\nCopy: %+v", settings, copy) + } + settings.vmodule.filter[1].pattern = "x" + if copy.vmodule.filter[1].pattern == settings.vmodule.filter[1].pattern { + t.Fatal("Copy should not have shared vmodule.filter.") + } +} From e6d69a51e74ab0a27dbcc00758de6b5a2a45eea0 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 9 May 2022 17:16:47 +0200 Subject: [PATCH 079/125] serialize: benchmark and test TrimDuplicates This is in preparation for enhancing performance of that function. --- internal/serialize/keyvalues_test.go | 155 +++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index affb887ec..dab2da1c6 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -19,6 +19,7 @@ package serialize_test import ( "bytes" "fmt" + "reflect" "testing" "time" @@ -149,3 +150,157 @@ No whitespace.`, } } } + +func TestDuplicates(t *testing.T) { + for name, test := range map[string]struct { + first, second []interface{} + expectedTrimmed [][]interface{} + }{ + "empty": { + expectedTrimmed: [][]interface{}{{}, {}}, + }, + "no-duplicates": { + first: makeKV("a", 3), + second: makeKV("b", 3), + expectedTrimmed: [][]interface{}{ + makeKV("a", 3), + makeKV("b", 3), + }, + }, + "all-duplicates": { + first: makeKV("a", 3), + second: makeKV("a", 3), + expectedTrimmed: [][]interface{}{ + {}, + makeKV("a", 3), + }, + }, + "start-duplicate": { + first: append([]interface{}{"x", 1}, makeKV("a", 3)...), + second: append([]interface{}{"x", 2}, makeKV("b", 3)...), + expectedTrimmed: [][]interface{}{ + makeKV("a", 3), + append([]interface{}{"x", 2}, makeKV("b", 3)...), + }, + }, + "subset-first": { + first: append([]interface{}{"x", 1}, makeKV("a", 3)...), + second: append([]interface{}{"x", 2}, makeKV("a", 3)...), + expectedTrimmed: [][]interface{}{ + {}, + append([]interface{}{"x", 2}, makeKV("a", 3)...), + }, + }, + "subset-second": { + first: append([]interface{}{"x", 1}, makeKV("a", 1)...), + second: append([]interface{}{"x", 2}, makeKV("b", 2)...), + expectedTrimmed: [][]interface{}{ + makeKV("a", 1), + append([]interface{}{"x", 2}, makeKV("b", 2)...), + }, + }, + "end-duplicate": { + first: append(makeKV("a", 3), "x", 1), + second: append(makeKV("b", 3), "x", 2), + expectedTrimmed: [][]interface{}{ + makeKV("a", 3), + append(makeKV("b", 3), "x", 2), + }, + }, + "middle-duplicate": { + first: []interface{}{"a-0", 0, "x", 1, "a-1", 2}, + second: []interface{}{"b-0", 0, "x", 2, "b-1", 2}, + expectedTrimmed: [][]interface{}{ + {"a-0", 0, "a-1", 2}, + {"b-0", 0, "x", 2, "b-1", 2}, + }, + }, + "internal-duplicates": { + first: []interface{}{"a", 0, "x", 1, "a", 2}, + second: []interface{}{"b", 0, "x", 2, "b", 2}, + expectedTrimmed: [][]interface{}{ + {"a", 2}, + {"x", 2, "b", 2}, + }, + }, + } { + t.Run(name, func(t *testing.T) { + actual := serialize.TrimDuplicates(test.first, test.second) + expectEqual(t, "trimmed key/value pairs", test.expectedTrimmed, actual) + }) + } +} + +// BenchmarkTrimDuplicates checks performance when TrimDuplicates is called with two slices. +// In practice that is how the function is used. +func BenchmarkTrimDuplicates(b *testing.B) { + for firstLength := 0; firstLength < 10; firstLength++ { + firstA := makeKV("a", firstLength) + for secondLength := 0; secondLength < 10; secondLength++ { + secondA := makeKV("a", secondLength) + secondB := makeKV("b", secondLength) + b.Run(fmt.Sprintf("%dx%d", firstLength, secondLength), func(b *testing.B) { + // This is the most common case: all key/value pairs are kept. + b.Run("no-duplicates", func(b *testing.B) { + expected := [][]interface{}{firstA, secondB} + benchTrimDuplicates(b, expected, firstA, secondB) + }) + + // Fairly unlikely... + b.Run("all-duplicates", func(b *testing.B) { + expected := [][]interface{}{{}, secondA} + if firstLength > secondLength { + expected[0] = append(expected[0], firstA[secondLength*2:]...) + } + benchTrimDuplicates(b, expected, firstA, secondA) + }) + + // First entry is the same. + b.Run("start-duplicate", func(b *testing.B) { + first := []interface{}{"x", 1} + first = append(first, firstA...) + second := []interface{}{"x", 1} + second = append(second, secondB...) + expected := [][]interface{}{firstA, second} + benchTrimDuplicates(b, expected, first, second) + }) + + // Last entry is the same. + b.Run("end-duplicate", func(b *testing.B) { + first := firstA[:] + first = append(first, "x", 1) + second := secondB[:] + second = append(second, "x", 1) + expected := [][]interface{}{firstA, second} + benchTrimDuplicates(b, expected, first, second) + }) + }) + } + } +} + +func makeKV(prefix string, length int) []interface{} { + if length == 0 { + return []interface{}{} + } + kv := make([]interface{}, 0, length*2) + for i := 0; i < length; i++ { + kv = append(kv, fmt.Sprintf("%s-%d", prefix, i), i) + } + return kv +} + +func benchTrimDuplicates(b *testing.B, expected interface{}, first, second []interface{}) { + actual := serialize.TrimDuplicates(first, second) + expectEqual(b, "trimmed key/value pairs", expected, actual) + b.ResetTimer() + for i := 0; i < b.N; i++ { + serialize.TrimDuplicates(first, second) + } +} + +func expectEqual(tb testing.TB, what string, expected, actual interface{}) { + if !reflect.DeepEqual(expected, actual) { + tb.Fatalf("Did not get correct %s. Expected:\n %v\nActual:\n %v", what, expected, actual) + } +} From e2432e8fa93f2873f99ab7fb741dbf855e5fb274 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 May 2022 08:09:46 +0200 Subject: [PATCH 080/125] serialize: add MergeKVs as faster TrimDuplicates alternative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrimDuplicates is unnecessarily complex: it takes and returns a variable number of slices when in practice it is always called with a pair of slices and the result is always merged into a single slice. It is also slow and does a lot of allocations. MergeKVs is a simpler implementation that is good enough for merging the WithValues key/value slice with the key/value parameters. It intentionally skips some unnecessary error checking (the WithValues key/value slice was already sanitized), does not handle individual slices that themselves contain duplicates (because that can be avoided through code review and/or static code analysis), and does not handle all potential duplicates (in practice, all keys are string constants). Because of this, it is considerably faster: name old time/op new time/op delta TrimDuplicates/0x0/no-duplicates-36 177ns ± 3% 2ns ± 0% -98.76% (p=0.008 n=5+5) ... TrimDuplicates/9x9/end-duplicate-36 14.1µs ± 1% 3.0µs ± 1% -78.39% (p=0.008 n=5+5) name old alloc/op new alloc/op delta TrimDuplicates/0x0/no-duplicates-36 48.0B ± 0% 0.0B -100.00% (p=0.008 n=5+5) ... TrimDuplicates/9x9/no-duplicates-36 4.35kB ± 0% 0.90kB ± 0% -79.42% (p=0.008 n=5+5) TrimDuplicates/9x9/all-duplicates-36 2.03kB ± 0% 0.90kB ± 0% -55.91% (p=0.008 n=5+5) TrimDuplicates/9x9/start-duplicate-36 4.71kB ± 0% 0.96kB ± 0% -79.56% (p=0.008 n=5+5) TrimDuplicates/9x9/end-duplicate-36 4.71kB ± 0% 0.96kB ± 0% ~ (p=0.079 n=4+5) name old allocs/op new allocs/op delta TrimDuplicates/0x0/no-duplicates-36 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) ... TrimDuplicates/9x9/end-duplicate-36 39.0 ± 0% 2.0 ± 0% -94.87% (p=0.008 n=5+5) --- internal/serialize/keyvalues.go | 45 ++++++++++++++++++++++++++++ internal/serialize/keyvalues_test.go | 43 +++++++++++++++++++------- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index d89731368..e64dab36f 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -93,6 +93,51 @@ func TrimDuplicates(kvLists ...[]interface{}) [][]interface{} { return outs } +// MergeKVs deduplicates elements provided in two key/value slices. +// +// Keys in each slice are expected to be unique, so duplicates can only occur +// when the first and second slice contain the same key. When that happens, the +// key/value pair from the second slice is used. The first slice must be well-formed +// (= even key/value pairs). The second one may have a missing value, in which +// case the special "missing value" is added to the result. +func MergeKVs(first, second []interface{}) []interface{} { + maxLength := len(first) + (len(second)+1)/2*2 + if maxLength == 0 { + // Nothing to do at all. + return nil + } + + if len(first) == 0 && len(second)%2 == 0 { + // Nothing to be overridden, second slice is well-formed + // and can be used directly. + return second + } + + // Determine which keys are in the second slice so that we can skip + // them when iterating over the first one. The code intentionally + // favors performance over completeness: we assume that keys are string + // constants and thus compare equal when the string values are equal. A + // string constant being overridden by, for example, a fmt.Stringer is + // not handled. + overrides := map[interface{}]bool{} + for i := 0; i < len(second); i += 2 { + overrides[second[i]] = true + } + merged := make([]interface{}, 0, maxLength) + for i := 0; i+1 < len(first); i += 2 { + key := first[i] + if overrides[key] { + continue + } + merged = append(merged, key, first[i+1]) + } + merged = append(merged, second...) + if len(merged)%2 != 0 { + merged = append(merged, missingValue) + } + return merged +} + const missingValue = "(MISSING)" // KVListFormat serializes all key/value pairs into the provided buffer. diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index dab2da1c6..7fd95e7e9 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -155,6 +155,7 @@ func TestDuplicates(t *testing.T) { for name, test := range map[string]struct { first, second []interface{} expectedTrimmed [][]interface{} + expectedMerged []interface{} }{ "empty": { expectedTrimmed: [][]interface{}{{}, {}}, @@ -166,6 +167,7 @@ func TestDuplicates(t *testing.T) { makeKV("a", 3), makeKV("b", 3), }, + expectedMerged: append(makeKV("a", 3), makeKV("b", 3)...), }, "all-duplicates": { first: makeKV("a", 3), @@ -174,6 +176,7 @@ func TestDuplicates(t *testing.T) { {}, makeKV("a", 3), }, + expectedMerged: makeKV("a", 3), }, "start-duplicate": { first: append([]interface{}{"x", 1}, makeKV("a", 3)...), @@ -182,6 +185,7 @@ func TestDuplicates(t *testing.T) { makeKV("a", 3), append([]interface{}{"x", 2}, makeKV("b", 3)...), }, + expectedMerged: append(append(makeKV("a", 3), "x", 2), makeKV("b", 3)...), }, "subset-first": { first: append([]interface{}{"x", 1}, makeKV("a", 3)...), @@ -190,6 +194,7 @@ func TestDuplicates(t *testing.T) { {}, append([]interface{}{"x", 2}, makeKV("a", 3)...), }, + expectedMerged: append([]interface{}{"x", 2}, makeKV("a", 3)...), }, "subset-second": { first: append([]interface{}{"x", 1}, makeKV("a", 1)...), @@ -198,6 +203,7 @@ func TestDuplicates(t *testing.T) { makeKV("a", 1), append([]interface{}{"x", 2}, makeKV("b", 2)...), }, + expectedMerged: append(append(makeKV("a", 1), "x", 2), makeKV("b", 2)...), }, "end-duplicate": { first: append(makeKV("a", 3), "x", 1), @@ -206,6 +212,7 @@ func TestDuplicates(t *testing.T) { makeKV("a", 3), append(makeKV("b", 3), "x", 2), }, + expectedMerged: append(makeKV("a", 3), append(makeKV("b", 3), "x", 2)...), }, "middle-duplicate": { first: []interface{}{"a-0", 0, "x", 1, "a-1", 2}, @@ -214,6 +221,7 @@ func TestDuplicates(t *testing.T) { {"a-0", 0, "a-1", 2}, {"b-0", 0, "x", 2, "b-1", 2}, }, + expectedMerged: []interface{}{"a-0", 0, "a-1", 2, "b-0", 0, "x", 2, "b-1", 2}, }, "internal-duplicates": { first: []interface{}{"a", 0, "x", 1, "a", 2}, @@ -222,11 +230,22 @@ func TestDuplicates(t *testing.T) { {"a", 2}, {"x", 2, "b", 2}, }, + // This is the case where Merged keeps key/value pairs + // that were already duplicated inside the slices, for + // performance. + expectedMerged: []interface{}{"a", 0, "a", 2, "b", 0, "x", 2, "b", 2}, }, } { t.Run(name, func(t *testing.T) { - actual := serialize.TrimDuplicates(test.first, test.second) - expectEqual(t, "trimmed key/value pairs", test.expectedTrimmed, actual) + t.Run("TrimDuplicates", func(t *testing.T) { + actual := serialize.TrimDuplicates(test.first, test.second) + expectEqual(t, "trimmed key/value pairs", test.expectedTrimmed, actual) + }) + + t.Run("Merged", func(t *testing.T) { + actual := serialize.MergeKVs(test.first, test.second) + expectEqual(t, "merged key/value pairs", test.expectedMerged, actual) + }) }) } } @@ -242,16 +261,17 @@ func BenchmarkTrimDuplicates(b *testing.B) { b.Run(fmt.Sprintf("%dx%d", firstLength, secondLength), func(b *testing.B) { // This is the most common case: all key/value pairs are kept. b.Run("no-duplicates", func(b *testing.B) { - expected := [][]interface{}{firstA, secondB} + expected := append(firstA, secondB...) benchTrimDuplicates(b, expected, firstA, secondB) }) // Fairly unlikely... b.Run("all-duplicates", func(b *testing.B) { - expected := [][]interface{}{{}, secondA} + var expected []interface{} if firstLength > secondLength { - expected[0] = append(expected[0], firstA[secondLength*2:]...) + expected = firstA[secondLength*2:] } + expected = append(expected, secondA...) benchTrimDuplicates(b, expected, firstA, secondA) }) @@ -261,7 +281,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { first = append(first, firstA...) second := []interface{}{"x", 1} second = append(second, secondB...) - expected := [][]interface{}{firstA, second} + expected := append(firstA, second...) benchTrimDuplicates(b, expected, first, second) }) @@ -271,7 +291,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { first = append(first, "x", 1) second := secondB[:] second = append(second, "x", 1) - expected := [][]interface{}{firstA, second} + expected := append(firstA, second...) benchTrimDuplicates(b, expected, first, second) }) }) @@ -290,12 +310,15 @@ func makeKV(prefix string, length int) []interface{} { return kv } -func benchTrimDuplicates(b *testing.B, expected interface{}, first, second []interface{}) { - actual := serialize.TrimDuplicates(first, second) +func benchTrimDuplicates(b *testing.B, expected []interface{}, first, second []interface{}) { + if len(expected) == 0 { + expected = nil + } + actual := serialize.MergeKVs(first, second) expectEqual(b, "trimmed key/value pairs", expected, actual) b.ResetTimer() for i := 0; i < b.N; i++ { - serialize.TrimDuplicates(first, second) + serialize.MergeKVs(first, second) } } From 6f47a32bc641bf2b345056d841a06831276dfd28 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 May 2022 08:55:49 +0200 Subject: [PATCH 081/125] klogr: simplify test failure message The mismatch is easier to see when the output is printed above each other instead of side-by-side. --- klogr/klogr_test.go | 2 +- ktesting/testinglogger_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go index ab3a525a5..7e95b9056 100644 --- a/klogr/klogr_test.go +++ b/klogr/klogr_test.go @@ -200,7 +200,7 @@ func testOutput(t *testing.T, format string) { expectedOutput = test.expectedKlogOutput } if actual != expectedOutput { - t.Errorf("expected %q did not match actual %q", expectedOutput, actual) + t.Errorf("Expected:\n%s\nActual:\n%s\n", expectedOutput, actual) } }) } diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index 430ece7c5..cc19c3b32 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -118,7 +118,7 @@ func TestInfo(t *testing.T) { actual := buffer.String() if actual != test.expectedOutput { - t.Errorf("expected %q did not match actual %q", test.expectedOutput, actual) + t.Errorf("Expected:\n%sActual:\n%s\n", test.expectedOutput, actual) } }) } From 4c3943c51f1386f8ce491a23b070f29f9936a4bc Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 May 2022 09:34:42 +0200 Subject: [PATCH 082/125] use faster MergeKVs Performance is better. This slightly changes the output of the traditional klogr format: - all key/value pairs gets sorted, not just each slice (an improvement?) - a single space is printed where previously two were printed klog no longer de-duplicates when the same key is passed twice in the same call (WithValues, InfoS, etc.). That should not occur in practice and can be prevented via code review and/or static code analysis, so we don't need to pay the price of checking for it at runtime. --- klogr.go | 8 +++---- klogr/klogr.go | 28 ++++++++++++---------- klogr/klogr_test.go | 44 ++++++++++++++++++---------------- ktesting/testinglogger.go | 10 ++++---- ktesting/testinglogger_test.go | 4 ++-- test/output.go | 16 +++++-------- test/zapr.go | 4 ++++ textlogger/textlogger.go | 5 ++-- 8 files changed, 60 insertions(+), 59 deletions(-) diff --git a/klogr.go b/klogr.go index 351d7a740..027a4014a 100644 --- a/klogr.go +++ b/klogr.go @@ -43,11 +43,11 @@ func (l *klogger) Init(info logr.RuntimeInfo) { } func (l klogger) Info(level int, msg string, kvList ...interface{}) { - trimmed := serialize.TrimDuplicates(l.values, kvList) + merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - V(Level(level)).InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...) + V(Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) } func (l klogger) Enabled(level int) bool { @@ -55,11 +55,11 @@ func (l klogger) Enabled(level int) bool { } func (l klogger) Error(err error, msg string, kvList ...interface{}) { - trimmed := serialize.TrimDuplicates(l.values, kvList) + merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - ErrorSDepth(l.callDepth+1, err, msg, append(trimmed[0], trimmed[1]...)...) + ErrorSDepth(l.callDepth+1, err, msg, merged...) } // WithName returns a new logr.Logger with the specified name appended. klogr diff --git a/klogr/klogr.go b/klogr/klogr.go index 48cb65815..c94e551ed 100644 --- a/klogr/klogr.go +++ b/klogr/klogr.go @@ -85,7 +85,11 @@ func flatten(kvList ...interface{}) string { if i+1 < len(kvList) { v = kvList[i+1] } - keys = append(keys, k) + // Only print each key once... + if _, seen := vals[k]; !seen { + keys = append(keys, k) + } + // ... with the latest value. vals[k] = v } sort.Strings(keys) @@ -119,16 +123,15 @@ func (l klogger) Info(level int, msg string, kvList ...interface{}) { switch l.format { case FormatSerialize: msgStr := flatten("msg", msg) - trimmed := serialize.TrimDuplicates(l.values, kvList) - fixedStr := flatten(trimmed[0]...) - userStr := flatten(trimmed[1]...) - klog.V(klog.Level(level)).InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", fixedStr, " ", userStr) + merged := serialize.MergeKVs(l.values, kvList) + kvStr := flatten(merged...) + klog.V(klog.Level(level)).InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", kvStr) case FormatKlog: - trimmed := serialize.TrimDuplicates(l.values, kvList) + merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - klog.V(klog.Level(level)).InfoSDepth(l.callDepth+1, msg, append(trimmed[0], trimmed[1]...)...) + klog.V(klog.Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) } } @@ -145,16 +148,15 @@ func (l klogger) Error(err error, msg string, kvList ...interface{}) { switch l.format { case FormatSerialize: errStr := flatten("error", loggableErr) - 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) + merged := serialize.MergeKVs(l.values, kvList) + kvStr := flatten(merged...) + klog.ErrorDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", errStr, " ", kvStr) case FormatKlog: - trimmed := serialize.TrimDuplicates(l.values, kvList) + merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - klog.ErrorSDepth(l.callDepth+1, err, msg, append(trimmed[0], trimmed[1]...)...) + klog.ErrorSDepth(l.callDepth+1, err, msg, merged...) } } diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go index 7e95b9056..22ec1dc30 100644 --- a/klogr/klogr_test.go +++ b/klogr/klogr_test.go @@ -41,7 +41,7 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0), text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: ` "msg"="test" "akey"="avalue" + expectedOutput: ` "msg"="test" "akey"="avalue" `, expectedKlogOutput: `"test" akey="avalue" `, @@ -50,7 +50,7 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0).WithName("me"), text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `me "msg"="test" "akey"="avalue" + expectedOutput: `me "msg"="test" "akey"="avalue" `, expectedKlogOutput: `"me: test" akey="avalue" `, @@ -59,34 +59,34 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0).WithName("hello").WithName("world"), text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `hello/world "msg"="test" "akey"="avalue" + expectedOutput: `hello/world "msg"="test" "akey"="avalue" `, expectedKlogOutput: `"hello/world: test" akey="avalue" `, }, - "should not print duplicate keys with the same value": { + "may print duplicate keys with the same value": { klogr: new().V(0), text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"}, - expectedOutput: ` "msg"="test" "akey"="avalue" + expectedOutput: ` "msg"="test" "akey"="avalue" `, - expectedKlogOutput: `"test" akey="avalue" + expectedKlogOutput: `"test" akey="avalue" akey="avalue" `, }, - "should only print the last duplicate key when the values are passed to Info": { + "may print duplicate keys when the values are passed to Info": { klogr: new().V(0), text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"}, - expectedOutput: ` "msg"="test" "akey"="avalue2" + expectedOutput: ` "msg"="test" "akey"="avalue2" `, - expectedKlogOutput: `"test" akey="avalue2" + expectedKlogOutput: `"test" akey="avalue" akey="avalue2" `, }, "should only print the duplicate key that is passed to Info if one was passed to the logger": { klogr: new().WithValues("akey", "avalue"), text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: ` "msg"="test" "akey"="avalue" + expectedOutput: ` "msg"="test" "akey"="avalue" `, expectedKlogOutput: `"test" akey="avalue" `, @@ -95,7 +95,7 @@ func testOutput(t *testing.T, format string) { klogr: new().WithValues("akey9", "avalue9", "akey8", "avalue8", "akey1", "avalue1"), text: "test", keysAndValues: []interface{}{"akey5", "avalue5", "akey4", "avalue4"}, - expectedOutput: ` "msg"="test" "akey1"="avalue1" "akey8"="avalue8" "akey9"="avalue9" "akey4"="avalue4" "akey5"="avalue5" + expectedOutput: ` "msg"="test" "akey1"="avalue1" "akey4"="avalue4" "akey5"="avalue5" "akey8"="avalue8" "akey9"="avalue9" `, expectedKlogOutput: `"test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4" `, @@ -104,7 +104,7 @@ func testOutput(t *testing.T, format string) { klogr: new().WithValues("akey", "avalue"), text: "test", keysAndValues: []interface{}{"akey", "avalue2"}, - expectedOutput: ` "msg"="test" "akey"="avalue2" + expectedOutput: ` "msg"="test" "akey"="avalue2" `, expectedKlogOutput: `"test" akey="avalue2" `, @@ -113,7 +113,7 @@ func testOutput(t *testing.T, format string) { klogr: new(), text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)" + expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)" `, expectedKlogOutput: `"test" akey="avalue" akey2="(MISSING)" `, @@ -122,7 +122,8 @@ func testOutput(t *testing.T, format string) { klogr: new().WithValues("keyWithoutValue"), text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: ` "msg"="test" "keyWithoutValue"="(MISSING)" "akey"="avalue" "akey2"="(MISSING)" + // klogr format sorts all key/value pairs. + expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)" "keyWithoutValue"="(MISSING)" `, expectedKlogOutput: `"test" keyWithoutValue="(MISSING)" akey="avalue" akey2="(MISSING)" `, @@ -131,7 +132,7 @@ func testOutput(t *testing.T, format string) { klogr: new(), text: "test", keysAndValues: []interface{}{"akey", "<&>"}, - expectedOutput: ` "msg"="test" "akey"="<&>" + expectedOutput: ` "msg"="test" "akey"="<&>" `, expectedKlogOutput: `"test" akey="<&>" `, @@ -140,7 +141,8 @@ 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"="(MISSING)" "akey"="avalue" "akey2"="(MISSING)" + // klogr format sorts all key/value pairs. + expectedOutput: ` "msg"="test" "akey"="avalue" "akey2"="(MISSING)" "basekey1"="basevar1" "basekey2"="(MISSING)" `, expectedKlogOutput: `"test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" `, @@ -149,7 +151,7 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0), text: "test", keysAndValues: []interface{}{"err", errors.New("whoops")}, - expectedOutput: ` "msg"="test" "err"="whoops" + expectedOutput: ` "msg"="test" "err"="whoops" `, expectedKlogOutput: `"test" err="whoops" `, @@ -158,7 +160,7 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0), text: "test", keysAndValues: []interface{}{"err", &customErrorJSON{"whoops"}}, - expectedOutput: ` "msg"="test" "err"="WHOOPS" + expectedOutput: ` "msg"="test" "err"="WHOOPS" `, expectedKlogOutput: `"test" err="whoops" `, @@ -168,9 +170,9 @@ func testOutput(t *testing.T, format string) { text: "test", err: errors.New("whoops"), // The message is printed to three different log files (info, warning, error), so we see it three times in our output buffer. - expectedOutput: ` "msg"="test" "error"="whoops" - "msg"="test" "error"="whoops" - "msg"="test" "error"="whoops" + expectedOutput: ` "msg"="test" "error"="whoops" + "msg"="test" "error"="whoops" + "msg"="test" "error"="whoops" `, expectedKlogOutput: `"test" err="whoops" "test" err="whoops" diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 5269f89d2..d796f60aa 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -81,9 +81,8 @@ func (l *tlogger) GetCallStackHelper() func() { func (l *tlogger) Info(level int, msg string, kvList ...interface{}) { l.t.Helper() buffer := &bytes.Buffer{} - trimmed := serialize.TrimDuplicates(l.values, kvList) - serialize.KVListFormat(buffer, trimmed[0]...) - serialize.KVListFormat(buffer, trimmed[1]...) + merged := serialize.MergeKVs(l.values, kvList) + serialize.KVListFormat(buffer, merged...) l.log("INFO", msg, buffer) } @@ -97,9 +96,8 @@ func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { if err != nil { serialize.KVListFormat(buffer, "err", err) } - trimmed := serialize.TrimDuplicates(l.values, kvList) - serialize.KVListFormat(buffer, trimmed[0]...) - serialize.KVListFormat(buffer, trimmed[1]...) + merged := serialize.MergeKVs(l.values, kvList) + serialize.KVListFormat(buffer, merged...) l.log("ERROR", msg, buffer) } diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index cc19c3b32..6c7ae187d 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -46,13 +46,13 @@ func TestInfo(t *testing.T) { "should not print duplicate keys with the same value": { text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"}, - expectedOutput: `INFO test akey="avalue" + expectedOutput: `INFO test akey="avalue" akey="avalue" `, }, "should only print the last duplicate key when the values are passed to Info": { text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"}, - expectedOutput: `INFO test akey="avalue2" + expectedOutput: `INFO test akey="avalue" akey="avalue2" `, }, "should only print the duplicate key that is passed to Info if one was passed to the logger": { diff --git a/test/output.go b/test/output.go index bec12018d..4dd7ad407 100644 --- a/test/output.go +++ b/test/output.go @@ -212,16 +212,12 @@ I output.go:] "test" firstKey=1 secondKey=3 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" - // `, - // }, + "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", diff --git a/test/zapr.go b/test/zapr.go index 20b118aad..5612d763f 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -35,6 +35,10 @@ func ZaprOutputMappingDirect() map[string]string { `I output.go:] "helper" akey="avalue" `: `{"caller":"test/output.go:","msg":"helper","v":0,"akey":"avalue"} +`, + + `I output.go:] "test" akey="avalue" akey="avalue2" +`: `{"caller":"test/output.go:","msg":"test","v":0,"akey":"avalue","akey":"avalue2"} `, `I output.go:] "hello/world: test" akey="avalue" diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 2c0ec88ff..17c7584f6 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -132,9 +132,8 @@ func (l *tlogger) print(err error, s severity.Severity, msg string, kvList []int 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]...) + merged := serialize.MergeKVs(l.values, kvList) + serialize.KVListFormat(&b.Buffer, merged...) if b.Len() == 0 || b.Bytes()[b.Len()-1] != '\n' { b.WriteByte('\n') } From 8a3e48ebe5c83cc1ac42e3c0c88d351e9ee41668 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 27 Apr 2022 14:53:53 +0200 Subject: [PATCH 083/125] replace KObjs with KObjSlice KObjSlice can be used in log calls without causing unnecessary work when the log event does not get logged. --- examples/benchmarks/benchmarks_test.go | 2 +- k8s_references.go | 64 ++++++++++++++++++++++++++ test/output.go | 41 +++++++++++++++++ test/zapr.go | 20 ++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/examples/benchmarks/benchmarks_test.go b/examples/benchmarks/benchmarks_test.go index eb8d5ae71..3105abafe 100644 --- a/examples/benchmarks/benchmarks_test.go +++ b/examples/benchmarks/benchmarks_test.go @@ -75,7 +75,7 @@ func BenchmarkOutput(b *testing.B) { tests = append(tests, testcase{ name: fmt.Sprintf("objects/%d", length), generate: func() interface{} { - return klog.KObjs(arg) + return klog.KObjSlice(arg) }, }) } diff --git a/k8s_references.go b/k8s_references.go index db58f8baa..2c218f698 100644 --- a/k8s_references.go +++ b/k8s_references.go @@ -77,6 +77,8 @@ func KRef(namespace, name string) ObjectRef { } // KObjs returns slice of ObjectRef from an slice of ObjectMeta +// +// DEPRECATED: Use KObjSlice instead, it has better performance. func KObjs(arg interface{}) []ObjectRef { s := reflect.ValueOf(arg) if s.Kind() != reflect.Slice { @@ -92,3 +94,65 @@ func KObjs(arg interface{}) []ObjectRef { } return objectRefs } + +// KObjSlice takes a slice of objects that implement the KMetadata interface +// and returns an object that gets logged as a slice of ObjectRef values or a +// string containing those values, depending on whether the logger prefers text +// output or structured output. +// +// An error string is logged when KObjSlice is not passed a suitable slice. +// +// Processing of the argument is delayed until the value actually gets logged, +// in contrast to KObjs where that overhead is incurred regardless of whether +// the result is needed. +func KObjSlice(arg interface{}) interface{} { + return kobjSlice{arg: arg} +} + +type kobjSlice struct { + arg interface{} +} + +var _ fmt.Stringer = kobjSlice{} +var _ logr.Marshaler = kobjSlice{} + +func (ks kobjSlice) String() string { + objectRefs, err := ks.process() + if err != nil { + return err.Error() + } + return fmt.Sprintf("%v", objectRefs) +} + +func (ks kobjSlice) MarshalLog() interface{} { + objectRefs, err := ks.process() + if err != nil { + return err.Error() + } + return objectRefs +} + +func (ks kobjSlice) process() ([]interface{}, error) { + s := reflect.ValueOf(ks.arg) + switch s.Kind() { + case reflect.Invalid: + // nil parameter, print as nil. + return nil, nil + case reflect.Slice: + // Okay, handle below. + default: + return nil, fmt.Errorf("", ks.arg) + } + objectRefs := make([]interface{}, 0, s.Len()) + for i := 0; i < s.Len(); i++ { + item := s.Index(i).Interface() + if item == nil { + objectRefs = append(objectRefs, nil) + } else if v, ok := item.(KMetadata); ok { + objectRefs = append(objectRefs, KObj(v)) + } else { + return nil, fmt.Errorf("", item) + } + } + return objectRefs, nil +} diff --git a/test/output.go b/test/output.go index bec12018d..5325fed73 100644 --- a/test/output.go +++ b/test/output.go @@ -268,6 +268,47 @@ I output.go:] "test" firstKey=1 secondKey=3 &kmeta{Name: "pod-2", Namespace: "kube-system"}, })}, expectedOutput: `I output.go:] "test" pods=[kube-system/pod-1 kube-system/pod-2] +`, + }, + "KObjSlice okay": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice([]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]" +`, + }, + "KObjSlice nil arg": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice(nil)}, + expectedOutput: `I output.go:] "test" pods="[]" +`, + }, + "KObjSlice int arg": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice(1)}, + expectedOutput: `I output.go:] "test" pods="" +`, + }, + "KObjSlice nil entry": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice([]interface{}{ + &kmeta{Name: "pod-1", Namespace: "kube-system"}, + nil, + })}, + expectedOutput: `I output.go:] "test" pods="[kube-system/pod-1 ]" +`, + }, + "KObjSlice ints": { + text: "test", + values: []interface{}{"ints", + klog.KObjSlice([]int{1, 2, 3})}, + expectedOutput: `I output.go:] "test" ints="" `, }, "regular error types as value": { diff --git a/test/zapr.go b/test/zapr.go index 20b118aad..8bf1af563 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -67,6 +67,26 @@ func ZaprOutputMappingDirect() map[string]string { `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" 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" pods="[]" +`: `{"caller":"test/output.go:","msg":"test","v":0,"pods":null} +`, + + `I output.go:] "test" pods="" +`: `{"caller":"test/output.go:","msg":"test","v":0,"pods":""} +`, + + `I output.go:] "test" ints="" +`: `{"caller":"test/output.go:","msg":"test","v":0,"ints":""} +`, + + `I output.go:] "test" pods="[kube-system/pod-1 ]" +`: `{"caller":"test/output.go:","msg":"test","v":0,"pods":[{"name":"pod-1","namespace":"kube-system"},null]} `, `I output.go:] "test" akey="avalue" From 369b848b057609c44b95113c449392dc2056b7d0 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 May 2022 11:09:08 +0200 Subject: [PATCH 084/125] support logr.Marshaler A type that implements only logr.Marshaler but not fmt.Stringer is useful for delayed formatting. It is not required to return a struct. A plain string may also be useful. klog now supports such types by invoking MarshalLog once and then printing the value as string (including multi-line support) if that is the result or with the default "%+v". --- internal/serialize/keyvalues.go | 32 ++++++++++++++++++++++++++++++++ test/output.go | 17 ++++++++++++++++- test/zapr.go | 6 +++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index d89731368..a3350f721 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -20,6 +20,8 @@ import ( "bytes" "fmt" "strconv" + + "github.com/go-logr/logr" ) // WithValues implements LogSink.WithValues. The old key/value pairs are @@ -131,6 +133,24 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { writeStringValue(b, true, v) case error: writeStringValue(b, true, ErrorToString(v)) + case logr.Marshaler: + value := MarshalerToValue(v) + // A marshaler that returns a string is useful for + // delayed formatting of complex values. We treat this + // case like a normal string. This is useful for + // multi-line support. + // + // We could do this by recursively formatting a value, + // but that comes with the risk of infinite recursion + // if a marshaler returns itself. Instead we call it + // only once and rely on it returning the intended + // value directly. + switch value := value.(type) { + case string: + writeStringValue(b, true, value) + default: + writeStringValue(b, false, fmt.Sprintf("%+v", v)) + } case []byte: // In https://github.com/kubernetes/klog/pull/237 it was decided // to format byte slices with "%+q". The advantages of that are: @@ -163,6 +183,18 @@ func StringerToString(s fmt.Stringer) (ret string) { return } +// MarshalerToValue invokes a marshaler and catches +// panics. +func MarshalerToValue(m logr.Marshaler) (ret interface{}) { + defer func() { + if err := recover(); err != nil { + ret = fmt.Sprintf("", err) + } + }() + ret = m.MarshalLog() + return +} + // ErrorToString converts an error to a string, // handling panics if they occur. func ErrorToString(err error) (ret string) { diff --git a/test/output.go b/test/output.go index bec12018d..d7f05b378 100644 --- a/test/output.go +++ b/test/output.go @@ -321,7 +321,13 @@ I output.go:] "test" firstKey=1 secondKey=3 "MarshalLog() that panics": { text: "marshaler panic", values: []interface{}{"obj", faultyMarshaler{}}, - expectedOutput: `I output.go:] "marshaler panic" obj={} + expectedOutput: `I output.go:] "marshaler panic" obj="" +`, + }, + "MarshalLog() that returns itself": { + text: "marshaler recursion", + values: []interface{}{"obj", recursiveMarshaler{}}, + expectedOutput: `I output.go:] "marshaler recursion" obj={} `, }, } @@ -765,6 +771,15 @@ func (f faultyMarshaler) MarshalLog() interface{} { var _ logr.Marshaler = faultyMarshaler{} +type recursiveMarshaler struct{} + +// MarshalLog returns itself, which could cause the logger to recurse infinitely. +func (r recursiveMarshaler) MarshalLog() interface{} { + return r +} + +var _ logr.Marshaler = recursiveMarshaler{} + type faultyError struct{} // Error always panics. diff --git a/test/zapr.go b/test/zapr.go index 20b118aad..ccf3af1ad 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -137,8 +137,12 @@ I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" `: `{"caller":"test/output.go:","msg":"error panic","errError":"PANIC=fake Error panic"} `, - `I output.go:] "marshaler panic" obj={} + `I output.go:] "marshaler panic" obj="" `: `{"caller":"test/output.go:","msg":"marshaler panic","v":0,"objError":"PANIC=fake MarshalLog panic"} +`, + + `I output.go:] "marshaler recursion" obj={} +`: `{"caller":"test/output.go:","msg":"marshaler recursion","v":0,"obj":{}} `, // klog.Info From cebb1902e68c5972ce2c7caae87129361438e2c6 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 May 2022 16:45:45 +0200 Subject: [PATCH 085/125] internal: remove unused TrimDuplicates Because the "internal" package is inaccessible for consumers of the klog/v2 API, removing code from it is okay. --- internal/serialize/keyvalues.go | 49 ------------- internal/serialize/keyvalues_test.go | 106 +++++++++------------------ 2 files changed, 33 insertions(+), 122 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index e64dab36f..14f7ec503 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -44,55 +44,6 @@ func WithValues(oldKV, newKV []interface{}) []interface{} { return kv } -// 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 - // 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. - // 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 -} - // MergeKVs deduplicates elements provided in two key/value slices. // // Keys in each slice are expected to be unique, so duplicates can only occur diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index 7fd95e7e9..79260092c 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -153,106 +153,66 @@ No whitespace.`, func TestDuplicates(t *testing.T) { for name, test := range map[string]struct { - first, second []interface{} - expectedTrimmed [][]interface{} - expectedMerged []interface{} + first, second []interface{} + expected []interface{} }{ - "empty": { - expectedTrimmed: [][]interface{}{{}, {}}, - }, + "empty": {}, "no-duplicates": { - first: makeKV("a", 3), - second: makeKV("b", 3), - expectedTrimmed: [][]interface{}{ - makeKV("a", 3), - makeKV("b", 3), - }, - expectedMerged: append(makeKV("a", 3), makeKV("b", 3)...), + first: makeKV("a", 3), + second: makeKV("b", 3), + expected: append(makeKV("a", 3), makeKV("b", 3)...), }, "all-duplicates": { - first: makeKV("a", 3), - second: makeKV("a", 3), - expectedTrimmed: [][]interface{}{ - {}, - makeKV("a", 3), - }, - expectedMerged: makeKV("a", 3), + first: makeKV("a", 3), + second: makeKV("a", 3), + expected: makeKV("a", 3), }, "start-duplicate": { - first: append([]interface{}{"x", 1}, makeKV("a", 3)...), - second: append([]interface{}{"x", 2}, makeKV("b", 3)...), - expectedTrimmed: [][]interface{}{ - makeKV("a", 3), - append([]interface{}{"x", 2}, makeKV("b", 3)...), - }, - expectedMerged: append(append(makeKV("a", 3), "x", 2), makeKV("b", 3)...), + first: append([]interface{}{"x", 1}, makeKV("a", 3)...), + second: append([]interface{}{"x", 2}, makeKV("b", 3)...), + expected: append(append(makeKV("a", 3), "x", 2), makeKV("b", 3)...), }, "subset-first": { - first: append([]interface{}{"x", 1}, makeKV("a", 3)...), - second: append([]interface{}{"x", 2}, makeKV("a", 3)...), - expectedTrimmed: [][]interface{}{ - {}, - append([]interface{}{"x", 2}, makeKV("a", 3)...), - }, - expectedMerged: append([]interface{}{"x", 2}, makeKV("a", 3)...), + first: append([]interface{}{"x", 1}, makeKV("a", 3)...), + second: append([]interface{}{"x", 2}, makeKV("a", 3)...), + expected: append([]interface{}{"x", 2}, makeKV("a", 3)...), }, "subset-second": { - first: append([]interface{}{"x", 1}, makeKV("a", 1)...), - second: append([]interface{}{"x", 2}, makeKV("b", 2)...), - expectedTrimmed: [][]interface{}{ - makeKV("a", 1), - append([]interface{}{"x", 2}, makeKV("b", 2)...), - }, - expectedMerged: append(append(makeKV("a", 1), "x", 2), makeKV("b", 2)...), + first: append([]interface{}{"x", 1}, makeKV("a", 1)...), + second: append([]interface{}{"x", 2}, makeKV("b", 2)...), + expected: append(append(makeKV("a", 1), "x", 2), makeKV("b", 2)...), }, "end-duplicate": { - first: append(makeKV("a", 3), "x", 1), - second: append(makeKV("b", 3), "x", 2), - expectedTrimmed: [][]interface{}{ - makeKV("a", 3), - append(makeKV("b", 3), "x", 2), - }, - expectedMerged: append(makeKV("a", 3), append(makeKV("b", 3), "x", 2)...), + first: append(makeKV("a", 3), "x", 1), + second: append(makeKV("b", 3), "x", 2), + expected: append(makeKV("a", 3), append(makeKV("b", 3), "x", 2)...), }, "middle-duplicate": { - first: []interface{}{"a-0", 0, "x", 1, "a-1", 2}, - second: []interface{}{"b-0", 0, "x", 2, "b-1", 2}, - expectedTrimmed: [][]interface{}{ - {"a-0", 0, "a-1", 2}, - {"b-0", 0, "x", 2, "b-1", 2}, - }, - expectedMerged: []interface{}{"a-0", 0, "a-1", 2, "b-0", 0, "x", 2, "b-1", 2}, + first: []interface{}{"a-0", 0, "x", 1, "a-1", 2}, + second: []interface{}{"b-0", 0, "x", 2, "b-1", 2}, + expected: []interface{}{"a-0", 0, "a-1", 2, "b-0", 0, "x", 2, "b-1", 2}, }, "internal-duplicates": { first: []interface{}{"a", 0, "x", 1, "a", 2}, second: []interface{}{"b", 0, "x", 2, "b", 2}, - expectedTrimmed: [][]interface{}{ - {"a", 2}, - {"x", 2, "b", 2}, - }, // This is the case where Merged keeps key/value pairs // that were already duplicated inside the slices, for // performance. - expectedMerged: []interface{}{"a", 0, "a", 2, "b", 0, "x", 2, "b", 2}, + expected: []interface{}{"a", 0, "a", 2, "b", 0, "x", 2, "b", 2}, }, } { t.Run(name, func(t *testing.T) { - t.Run("TrimDuplicates", func(t *testing.T) { - actual := serialize.TrimDuplicates(test.first, test.second) - expectEqual(t, "trimmed key/value pairs", test.expectedTrimmed, actual) - }) - t.Run("Merged", func(t *testing.T) { actual := serialize.MergeKVs(test.first, test.second) - expectEqual(t, "merged key/value pairs", test.expectedMerged, actual) + expectEqual(t, "merged key/value pairs", test.expected, actual) }) }) } } -// BenchmarkTrimDuplicates checks performance when TrimDuplicates is called with two slices. +// BenchmarkMergeKVs checks performance when MergeKVs is called with two slices. // In practice that is how the function is used. -func BenchmarkTrimDuplicates(b *testing.B) { +func BenchmarkMergeKVs(b *testing.B) { for firstLength := 0; firstLength < 10; firstLength++ { firstA := makeKV("a", firstLength) for secondLength := 0; secondLength < 10; secondLength++ { @@ -262,7 +222,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { // This is the most common case: all key/value pairs are kept. b.Run("no-duplicates", func(b *testing.B) { expected := append(firstA, secondB...) - benchTrimDuplicates(b, expected, firstA, secondB) + benchMergeKVs(b, expected, firstA, secondB) }) // Fairly unlikely... @@ -272,7 +232,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { expected = firstA[secondLength*2:] } expected = append(expected, secondA...) - benchTrimDuplicates(b, expected, firstA, secondA) + benchMergeKVs(b, expected, firstA, secondA) }) // First entry is the same. @@ -282,7 +242,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { second := []interface{}{"x", 1} second = append(second, secondB...) expected := append(firstA, second...) - benchTrimDuplicates(b, expected, first, second) + benchMergeKVs(b, expected, first, second) }) // Last entry is the same. @@ -292,7 +252,7 @@ func BenchmarkTrimDuplicates(b *testing.B) { second := secondB[:] second = append(second, "x", 1) expected := append(firstA, second...) - benchTrimDuplicates(b, expected, first, second) + benchMergeKVs(b, expected, first, second) }) }) } @@ -310,7 +270,7 @@ func makeKV(prefix string, length int) []interface{} { return kv } -func benchTrimDuplicates(b *testing.B, expected []interface{}, first, second []interface{}) { +func benchMergeKVs(b *testing.B, expected []interface{}, first, second []interface{}) { if len(expected) == 0 { expected = nil } From 49d73a5683e6bfbcfdea194835ccb4a59ad5803f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 10 Jun 2022 14:38:49 +0200 Subject: [PATCH 086/125] GitHub: use apidiff with more recent Go This fixes: Error: ../../../go/src/golang.org/x/exp/apidiff/correspondence.go:129:8: undefined: types.TypeParam --- .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 841ee58c9..30f0638b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - name: Install Go uses: actions/setup-go@v1 with: - go-version: 1.13.x + go-version: 1.18 - name: Add GOBIN to PATH run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV - name: Install dependencies From c9779195f2413cc49e54c4ae5f46e61ffcc40d3e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 13 Jun 2022 20:42:19 +0200 Subject: [PATCH 087/125] klog.Fatal: revert dumping of all stack backtraces to stderr This is a revert of https://github.com/kubernetes/klog/pull/79. Dumping the stack backtraces of all goroutines to stderr is not useful for processes which have many goroutines. The console buffer overflows and the original error which got dumped earlier is no longer visible. Even in CI systems where stderr is captured the full dump is often not useful. This was pointed out as a potential problem during the original PR review but only got more attention after the updated klog got merged into Kubernetes and developers there started to see the longer output. --- integration_tests/klog_test.go | 15 +++++++++------ klog.go | 11 +++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/integration_tests/klog_test.go b/integration_tests/klog_test.go index 565fd8ac4..33d0d9cc1 100644 --- a/integration_tests/klog_test.go +++ b/integration_tests/klog_test.go @@ -91,9 +91,10 @@ func TestDestinationsWithDifferentFlags(t *testing.T) { notExpectedInDir map[int]res }{ "default flags": { - // Everything, including the trace on fatal, goes to stderr + // Everything, EXCEPT the trace on fatal, goes to stderr - expectedOnStderr: allLogREs, + expectedOnStderr: res{infoLogRE, warningLogRE, errorLogRE, fatalLogRE}, + notExpectedOnStderr: res{stackTraceRE}, }, "everything disabled": { // Nothing, including the trace on fatal, is showing anywhere @@ -112,11 +113,12 @@ func TestDestinationsWithDifferentFlags(t *testing.T) { notExpectedOnStderr: res{infoLogRE}, }, "with logtostderr only": { - // Everything, including the trace on fatal, goes to stderr + // Everything, EXCEPT the trace on fatal, goes to stderr flags: []string{"-logtostderr=true", "-alsologtostderr=false", "-stderrthreshold=1000"}, - expectedOnStderr: allLogREs, + expectedOnStderr: res{infoLogRE, warningLogRE, errorLogRE, fatalLogRE}, + notExpectedOnStderr: res{stackTraceRE}, }, "with log file only": { // Everything, including the trace on fatal, goes to the single log file @@ -154,13 +156,14 @@ func TestDestinationsWithDifferentFlags(t *testing.T) { notExpectedInDir: defaultNotExpectedInDirREs, }, "with log dir and logtostderr": { - // Everything, including the trace on fatal, goes to stderr. The -log_dir is + // Everything, EXCEPT the trace on fatal, goes to stderr. The -log_dir is // ignored, nothing goes to the log files in the log dir. logdir: true, flags: []string{"-logtostderr=true", "-alsologtostderr=false", "-stderrthreshold=1000"}, - expectedOnStderr: allLogREs, + expectedOnStderr: res{infoLogRE, warningLogRE, errorLogRE, fatalLogRE}, + notExpectedOnStderr: res{stackTraceRE}, }, "with log file and log dir": { // Everything, including the trace on fatal, goes to the single log file. diff --git a/klog.go b/klog.go index 4ad2a5b4a..1a11f7814 100644 --- a/klog.go +++ b/klog.go @@ -924,12 +924,15 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf OsExit(1) } // Dump all goroutine stacks before exiting. - trace := stacks(true) - // Write the stack trace for all goroutines to the stderr. - if l.toStderr || l.alsoToStderr || s >= l.stderrThreshold.get() || alsoToStderr { - os.Stderr.Write(trace) + // First, make sure we see the trace for the current goroutine on standard error. + // If -logtostderr has been specified, the loop below will do that anyway + // as the first stack in the full dump. + if !l.toStderr { + os.Stderr.Write(stacks(false)) } + // Write the stack trace for all goroutines to the files. + trace := stacks(true) logExitFunc = func(error) {} // If we get a write error, we'll still exit below. for log := severity.FatalLog; log >= severity.InfoLog; log-- { if f := l.file[log]; f != nil { // Can be nil if -logtostderr is set. From e9884d225064b2e731decb8be5b157eae5ce188f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 14 Jun 2022 17:56:29 +0200 Subject: [PATCH 088/125] remove hack/tools The logcheck tool was moved to https://github.com/kubernetes-sigs/logtools and it seems unlikely that we'll want to add another, similar tool under hack/tools again, so the entire directory gets removed. --- .github/workflows/test.yml | 2 - README.md | 1 - hack/tools/go.mod | 8 - hack/tools/go.sum | 46 -- hack/tools/logcheck/README.md | 82 --- hack/tools/logcheck/main.go | 27 - hack/tools/logcheck/main_test.go | 126 ----- hack/tools/logcheck/pkg/filter.go | 129 ----- hack/tools/logcheck/pkg/filter_test.go | 161 ------ hack/tools/logcheck/pkg/logcheck.go | 498 ------------------ hack/tools/logcheck/plugin/plugin.go | 35 -- .../allowUnstructuredLogs.go | 67 --- .../testdata/src/contextual/contextual.go | 49 -- .../doNotAllowUnstructuredLogs.go | 67 --- .../src/github.com/go-logr/logr/logr.go | 36 -- .../logcheck/testdata/src/gologr/gologr.go | 38 -- .../src/helpers/doNotAllowDirectCalls.go | 43 -- .../testdata/src/importrename/importrename.go | 30 -- .../testdata/src/k8s.io/klog/v2/klog.go | 250 --------- .../src/mixed/allowUnstructuredLogs.go | 31 -- .../src/mixed/doNotAllowUnstructuredLogs.go | 30 -- .../testdata/src/mixed/structured_logging | 6 - .../src/onlyAllowContextual/klog_logging | 1 - .../onlyAllowContextual.go | 32 -- .../onlyAllowContextual/whitelistedKlog.go | 37 -- .../testdata/src/parameters/parameters.go | 55 -- .../logcheck/testdata/src/verbose/verbose.go | 57 -- 27 files changed, 1944 deletions(-) delete mode 100644 hack/tools/go.mod delete mode 100644 hack/tools/go.sum delete mode 100644 hack/tools/logcheck/README.md delete mode 100644 hack/tools/logcheck/main.go delete mode 100644 hack/tools/logcheck/main_test.go delete mode 100644 hack/tools/logcheck/pkg/filter.go delete mode 100644 hack/tools/logcheck/pkg/filter_test.go delete mode 100644 hack/tools/logcheck/pkg/logcheck.go delete mode 100644 hack/tools/logcheck/plugin/plugin.go delete mode 100644 hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go delete mode 100644 hack/tools/logcheck/testdata/src/contextual/contextual.go delete mode 100644 hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go delete mode 100644 hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go delete mode 100644 hack/tools/logcheck/testdata/src/gologr/gologr.go delete mode 100644 hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go delete mode 100644 hack/tools/logcheck/testdata/src/importrename/importrename.go delete mode 100644 hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go delete mode 100644 hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go delete mode 100644 hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go delete mode 100644 hack/tools/logcheck/testdata/src/mixed/structured_logging delete mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging delete mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go delete mode 100644 hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go delete mode 100644 hack/tools/logcheck/testdata/src/parameters/parameters.go delete mode 100644 hack/tools/logcheck/testdata/src/verbose/verbose.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 841ee58c9..ac3b210fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,6 @@ jobs: run: | go get -t -v ./... go test -v -race ./... - - name: Test hack/tools - run: cd hack/tools && go test -v -race ./... - name: Test examples run: cd examples && go test -v -race ./... lint: diff --git a/README.md b/README.md index 7de2212cc..d45cbe172 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ Historical context is available here: 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/hack/tools` - no stable API yet (may change eventually or get moved to separate repo), `hack/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.) diff --git a/hack/tools/go.mod b/hack/tools/go.mod deleted file mode 100644 index 3c94929c9..000000000 --- a/hack/tools/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module k8s.io/klog/hack/tools - -go 1.15 - -require ( - golang.org/x/exp v0.0.0-20210220032938-85be41e4509f - golang.org/x/tools v0.1.0 -) diff --git a/hack/tools/go.sum b/hack/tools/go.sum deleted file mode 100644 index e89e59cf9..000000000 --- a/hack/tools/go.sum +++ /dev/null @@ -1,46 +0,0 @@ -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20210220032938-85be41e4509f h1:GrkO5AtFUU9U/1f5ctbIBXtBGeSJbWwIYfIsTcFMaX4= -golang.org/x/exp v0.0.0-20210220032938-85be41e4509f/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/hack/tools/logcheck/README.md b/hack/tools/logcheck/README.md deleted file mode 100644 index 1e96afbcd..000000000 --- a/hack/tools/logcheck/README.md +++ /dev/null @@ -1,82 +0,0 @@ -This directory contains a linter for checking log calls. It was originally -created to detect when unstructured logging calls like `klog.Infof` get added -to files that should only use structured logging calls like `klog.InfoS` -and now also supports other checks. - -# Installation - -`go install k8s.io/klog/hack/tools/logcheck` - -# Usage - -`$logcheck.go ` -`e.g $logcheck ./pkg/kubelet/lifecycle/` - -# Configuration - -Checks can be enabled or disabled globally via command line flags and env -variables. In addition, the global setting for a check can be modified per file -via a configuration file. That file contains lines in this format: - -``` - -``` - -`` is a comma-separated list of the names of checks that get enabled or -disabled when a file name matches the regular expression. A check gets disabled -when its name has `-` as prefix and enabled when there is no prefix or `+` as -prefix. Only checks that are mentioned explicitly are modified. All regular -expressions are checked in order, so later lines can override the previous -ones. - -In this example, checking for klog calls is enabled for all files under -`pkg/scheduler` in the Kubernetes repo except for `scheduler.go` -itself. Parameter checking is disabled everywhere. - -``` -klog,-parameters k8s.io/kubernetes/pkg/scheduler/.* --klog k8s.io/kubernetes/pkg/scheduler/scheduler.go -``` - -The names of all supported checks are the ones used as sub-section titles in -the next section. - -# Checks - -## structured (enabled by default) - -Unstructured klog logging calls are flagged as error. - -## klog (disabled by default) - -None of the klog logging methods may be used. This is even stricter than -`unstructured`. Instead, code should retrieve a logr.Logger from klog and log -through that. - -## parameters (enabled by default) - -This ensures that if certain logging functions are allowed and are used, those -functions are passed correct parameters. - -### all calls - -Format strings are not allowed where plain strings are expected. - -### structured logging calls - -Key/value parameters for logging calls are checked: -- For each key there must be a value. -- Keys must be constant strings. - -This also warns about code that is valid, for example code that collects -key/value pairs in an `[]interface` variable before passing that on to a log -call. Such valid code can use `nolint:logcheck` to disable the warning (when -invoking logcheck through golangci-lint) or the `parameters` check can be -disabled for the file. - -## with-helpers (disabled by default) - -`logr.Logger.WithName`, `logr.Logger.WithValues` and `logr.NewContext` must not -be used. The corresponding helper calls from `k8s.io/klogr` should be used -instead. This is relevant when support contextual logging is disabled at -runtime in klog. diff --git a/hack/tools/logcheck/main.go b/hack/tools/logcheck/main.go deleted file mode 100644 index de5fd7e4b..000000000 --- a/hack/tools/logcheck/main.go +++ /dev/null @@ -1,27 +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 main - -import ( - "golang.org/x/tools/go/analysis/singlechecker" - - "k8s.io/klog/hack/tools/logcheck/pkg" -) - -func main() { - singlechecker.Main(pkg.Analyser()) -} diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go deleted file mode 100644 index e759154b5..000000000 --- a/hack/tools/logcheck/main_test.go +++ /dev/null @@ -1,126 +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 main - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" - - "k8s.io/klog/hack/tools/logcheck/pkg" -) - -func TestAnalyzer(t *testing.T) { - tests := []struct { - name string - enabled map[string]string - override string - testPackage string - }{ - { - name: "Allow unstructured logs", - enabled: map[string]string{ - "structured": "false", - }, - testPackage: "allowUnstructuredLogs", - }, - { - name: "Do not allow unstructured logs", - testPackage: "doNotAllowUnstructuredLogs", - }, - { - name: "Per-file config", - enabled: map[string]string{ - "structured": "false", - }, - override: "testdata/src/mixed/structured_logging", - testPackage: "mixed", - }, - { - name: "Function call parameters", - enabled: map[string]string{ - "structured": "false", - }, - testPackage: "parameters", - }, - { - name: "Only allow contextual calls", - enabled: map[string]string{ - "structured": "true", - "contextual": "true", - }, - testPackage: "onlyAllowContextual", - }, - { - name: "Only allow contextual calls through config", - enabled: map[string]string{ - "structured": "false", - "contextual": "false", - }, - override: "testdata/src/onlyAllowContextual/klog_logging", - testPackage: "onlyAllowContextual", - }, - { - name: "importrename", - testPackage: "importrename", - }, - { - name: "verbose", - testPackage: "verbose", - }, - { - name: "gologr", - enabled: map[string]string{ - "contextual": "true", - }, - testPackage: "gologr", - }, - { - name: "contextual", - enabled: map[string]string{ - "contextual": "true", - }, - testPackage: "contextual", - }, - { - name: "helpers", - enabled: map[string]string{ - "with-helpers": "true", - }, - testPackage: "helpers", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - analyzer := pkg.Analyser() - set := func(flag, value string) { - if value != "" { - if err := analyzer.Flags.Set(flag, value); err != nil { - t.Fatalf("unexpected error for %s: %v", flag, err) - } - } - } - for key, value := range tc.enabled { - set("check-"+key, value) - } - if tc.override != "" { - set("config", tc.override) - } - analysistest.Run(t, analysistest.TestData(), analyzer, tc.testPackage) - }) - } -} diff --git a/hack/tools/logcheck/pkg/filter.go b/hack/tools/logcheck/pkg/filter.go deleted file mode 100644 index c2ad90925..000000000 --- a/hack/tools/logcheck/pkg/filter.go +++ /dev/null @@ -1,129 +0,0 @@ -/* -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 pkg - -import ( - "bufio" - "flag" - "fmt" - "os" - "regexp" - "strings" -) - -// RegexpFilter implements flag.Value by accepting a file name and parsing that -// file. -type RegexpFilter struct { - filename string - validChecks map[string]bool - lines []filter -} - -type filter struct { - enabled map[string]bool - match *regexp.Regexp -} - -var _ flag.Value = &RegexpFilter{} - -func (f *RegexpFilter) String() string { - return f.filename -} - -func (f *RegexpFilter) Set(filename string) error { - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - // Reset before parsing. - f.filename = filename - f.lines = nil - - // Read line-by-line. - scanner := bufio.NewScanner(file) - for lineNr := 0; scanner.Scan(); lineNr++ { - text := scanner.Text() - if strings.HasPrefix(text, "#") { - continue - } - text = strings.TrimSpace(text) - if text == "" { - continue - } - - line := filter{ - enabled: map[string]bool{}, - } - parts := strings.SplitN(text, " ", 2) - if len(parts) != 2 { - return fmt.Errorf("%s:%d: not of the format : %s", filename, lineNr, text) - } - for _, c := range strings.Split(parts[0], ",") { - enabled := true - if strings.HasPrefix(c, "+") { - c = c[1:] - } else if strings.HasPrefix(c, "-") { - enabled = false - c = c[1:] - } - if !f.validChecks[c] { - return fmt.Errorf("%s:%d: %q is not a supported check: %s", filename, lineNr, c, text) - } - line.enabled[c] = enabled - } - - re, err := regexp.Compile(parts[1]) - if err != nil { - return fmt.Errorf("%s:%d: %v", filename, lineNr, err) - } - line.match = re - f.lines = append(f.lines, line) - } - - if err := scanner.Err(); err != nil { - return err - } - return nil -} - -// Enabled checks whether a certain check is enabled for a file. -func (f *RegexpFilter) Enabled(check string, enabled bool, filename string) bool { - for _, l := range f.lines { - // Must match entire string. - if matchFullString(filename, l.match) { - if e, ok := l.enabled[check]; ok { - enabled = e - } - } - } - return enabled -} - -func matchFullString(str string, re *regexp.Regexp) bool { - loc := re.FindStringIndex(str) - if loc == nil { - // No match at all. - return false - } - if loc[1]-loc[0] < len(str) { - // Only matches a substring. - return false - } - return true -} diff --git a/hack/tools/logcheck/pkg/filter_test.go b/hack/tools/logcheck/pkg/filter_test.go deleted file mode 100644 index eee829a7b..000000000 --- a/hack/tools/logcheck/pkg/filter_test.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -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 pkg - -import ( - "io/ioutil" - "path" - "testing" -) - -func TestMatch(t *testing.T) { - temp := t.TempDir() - filename := path.Join(temp, "expressions") - if err := ioutil.WriteFile(filename, []byte(`# Example file -structured hello -+structured a.c --structured adc -structured x.*y -structured,parameters world -`), 0666); err != nil { - t.Fatalf("writing file: %v", err) - } - - filter := &RegexpFilter{ - validChecks: map[string]bool{ - structuredCheck: true, - parametersCheck: true, - }, - } - if err := filter.Set(filename); err != nil { - t.Fatalf("reading file: %v", err) - } - - for _, tc := range []struct { - filename string - check string - enabled bool - expectEnabled bool - }{ - { - filename: "hello", - check: "structured", - expectEnabled: true, - }, - { - filename: "hello", - check: "parameters", - expectEnabled: false, // not set - }, - { - filename: "hello", - check: "parameters", - enabled: true, - expectEnabled: true, // global default - }, - { - filename: "hello/world", - check: "structured", - expectEnabled: false, // no sub-matches - }, - { - filename: "abc", - check: "structured", - expectEnabled: true, - }, - { - filename: "adc", - check: "structured", - expectEnabled: false, // unset later - }, - { - filename: "x1y", - check: "structured", - expectEnabled: true, - }, - { - filename: "x2y", - check: "structured", - expectEnabled: true, - }, - } { - actualEnabled := filter.Enabled(tc.check, tc.enabled, tc.filename) - if actualEnabled != tc.expectEnabled { - t.Errorf("%+v: got %v", tc, actualEnabled) - } - } -} - -func TestSetNoFile(t *testing.T) { - filter := &RegexpFilter{} - if err := filter.Set("no such file"); err == nil { - t.Errorf("did not get expected error") - } -} - -func TestParsing(t *testing.T) { - temp := t.TempDir() - filename := path.Join(temp, "expressions") - for name, tc := range map[string]struct { - content string - expectError string - }{ - "invalid-regexp": { - content: `structured [`, - expectError: filename + ":0: error parsing regexp: missing closing ]: `[`", - }, - "wildcard": { - content: `structured *`, - expectError: filename + ":0: error parsing regexp: missing argument to repetition operator: `*`", - }, - "invalid-line": { - content: `structured . -parameters`, - expectError: filename + ":1: not of the format : parameters", - }, - "invalid-check": { - content: `xxx .`, - expectError: filename + ":0: \"xxx\" is not a supported check: xxx .", - }, - } { - t.Run(name, func(t *testing.T) { - if err := ioutil.WriteFile(filename, []byte(tc.content), 0666); err != nil { - t.Fatalf("writing file: %v", err) - } - - filter := &RegexpFilter{ - validChecks: map[string]bool{ - structuredCheck: true, - parametersCheck: true, - }, - } - err := filter.Set(filename) - if tc.expectError == "" { - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - } else { - if err == nil { - t.Fatalf("did not get expected error: %s", tc.expectError) - } - if err.Error() != tc.expectError { - t.Fatalf("error mismatch\nexpected: %q\n got: %q", tc.expectError, err.Error()) - } - } - }) - } -} diff --git a/hack/tools/logcheck/pkg/logcheck.go b/hack/tools/logcheck/pkg/logcheck.go deleted file mode 100644 index 38122de0e..000000000 --- a/hack/tools/logcheck/pkg/logcheck.go +++ /dev/null @@ -1,498 +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 pkg - -import ( - "flag" - "fmt" - "go/ast" - "go/token" - "go/types" - "os" - "path" - "strconv" - "strings" - - "golang.org/x/exp/utf8string" - "golang.org/x/tools/go/analysis" -) - -const ( - structuredCheck = "structured" - parametersCheck = "parameters" - contextualCheck = "contextual" - withHelpersCheck = "with-helpers" -) - -type checks map[string]*bool - -type config struct { - enabled checks - fileOverrides RegexpFilter -} - -func (c config) isEnabled(check string, filename string) bool { - return c.fileOverrides.Enabled(check, *c.enabled[check], filename) -} - -// Analyser creates a new logcheck analyser. -func Analyser() *analysis.Analyzer { - c := config{ - enabled: checks{ - structuredCheck: new(bool), - parametersCheck: new(bool), - contextualCheck: new(bool), - withHelpersCheck: new(bool), - }, - } - c.fileOverrides.validChecks = map[string]bool{} - for key := range c.enabled { - c.fileOverrides.validChecks[key] = true - } - logcheckFlags := flag.NewFlagSet("", flag.ExitOnError) - prefix := "check-" - logcheckFlags.BoolVar(c.enabled[structuredCheck], prefix+structuredCheck, true, `When true, logcheck will warn about calls to unstructured -klog methods (Info, Infof, Error, Errorf, Warningf, etc).`) - logcheckFlags.BoolVar(c.enabled[parametersCheck], prefix+parametersCheck, true, `When true, logcheck will check parameters of structured logging calls.`) - logcheckFlags.BoolVar(c.enabled[contextualCheck], prefix+contextualCheck, false, `When true, logcheck will only allow log calls for contextual logging (retrieving a Logger from klog or the context and logging through that) and warn about all others.`) - logcheckFlags.BoolVar(c.enabled[withHelpersCheck], prefix+withHelpersCheck, false, `When true, logcheck will warn about direct calls to WithName, WithValues and NewContext.`) - logcheckFlags.Var(&c.fileOverrides, "config", `A file which overrides the global settings for checks on a per-file basis via regular expressions.`) - - // Use env variables as defaults. This is necessary when used as plugin - // for golangci-lint because of - // https://github.com/golangci/golangci-lint/issues/1512. - for key, enabled := range c.enabled { - envVarName := "LOGCHECK_" + strings.ToUpper(strings.ReplaceAll(string(key), "-", "_")) - if value, ok := os.LookupEnv(envVarName); ok { - v, err := strconv.ParseBool(value) - if err != nil { - panic(fmt.Errorf("%s=%q: %v", envVarName, value, err)) - } - *enabled = v - } - } - if value, ok := os.LookupEnv("LOGCHECK_CONFIG"); ok { - if err := c.fileOverrides.Set(value); err != nil { - panic(fmt.Errorf("LOGCHECK_CONFIG=%q: %v", value, err)) - } - } - - return &analysis.Analyzer{ - Name: "logcheck", - Doc: "Tool to check logging calls.", - Run: func(pass *analysis.Pass) (interface{}, error) { - return run(pass, &c) - }, - Flags: *logcheckFlags, - } -} - -func run(pass *analysis.Pass, c *config) (interface{}, error) { - for _, file := range pass.Files { - ast.Inspect(file, func(n ast.Node) bool { - switch n := n.(type) { - case *ast.CallExpr: - // We are intrested in function calls, as we want to detect klog.* calls - // passing all function calls to checkForFunctionExpr - checkForFunctionExpr(n, pass, c) - case *ast.FuncType: - checkForContextAndLogger(n, n.Params, pass, c) - case *ast.IfStmt: - checkForIfEnabled(n, pass, c) - } - - return true - }) - } - return nil, nil -} - -// checkForFunctionExpr checks for unstructured logging function, prints error if found any. -func checkForFunctionExpr(fexpr *ast.CallExpr, pass *analysis.Pass, c *config) { - fun := fexpr.Fun - args := fexpr.Args - - /* we are extracting external package function calls e.g. klog.Infof fmt.Printf - and eliminating calls like setLocalHost() - basically function calls that has selector expression like . - */ - if selExpr, ok := fun.(*ast.SelectorExpr); ok { - // extracting function Name like Infof - fName := selExpr.Sel.Name - - filename := pass.Pkg.Path() + "/" + path.Base(pass.Fset.Position(fexpr.Pos()).Filename) - - // Now we need to determine whether it is coming from klog. - if isKlog(selExpr.X, pass) { - if c.isEnabled(contextualCheck, filename) && !isContextualCall(fName) { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("function %q should not be used, convert to contextual logging", fName), - }) - return - } - - // Matching if any unstructured logging function is used. - if c.isEnabled(structuredCheck, filename) && isUnstructured(fName) { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("unstructured logging function %q should not be used", fName), - }) - return - } - - if c.isEnabled(parametersCheck, filename) { - // if format specifier is used, check for arg length will most probably fail - // so check for format specifier first and skip if found - if checkForFormatSpecifier(fexpr, pass) { - return - } - if fName == "InfoS" { - isKeysValid(args[1:], fun, pass, fName) - } else if fName == "ErrorS" { - isKeysValid(args[2:], fun, pass, fName) - } - - // Also check structured calls. - if c.isEnabled(parametersCheck, filename) { - checkForFormatSpecifier(fexpr, pass) - } - } - } else if isGoLogger(selExpr.X, pass) { - if c.isEnabled(parametersCheck, filename) { - checkForFormatSpecifier(fexpr, pass) - switch fName { - case "WithValues": - isKeysValid(args, fun, pass, fName) - case "Info": - isKeysValid(args[1:], fun, pass, fName) - case "Error": - isKeysValid(args[2:], fun, pass, fName) - } - } - if c.isEnabled(withHelpersCheck, filename) { - switch fName { - case "WithValues", "WithName": - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("function %q should be called through klogr.Logger%s", fName, fName), - }) - } - } - } else if fName == "NewContext" && - isPackage(selExpr.X, "github.com/go-logr/logr", pass) && - c.isEnabled(withHelpersCheck, filename) { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("function %q should be called through klogr.NewContext", fName), - }) - } - - } -} - -// isKlogVerbose returns true if the type of the expression is klog.Verbose (= -// the result of klog.V). -func isKlogVerbose(expr ast.Expr, pass *analysis.Pass) bool { - if typeAndValue, ok := pass.TypesInfo.Types[expr]; ok { - switch t := typeAndValue.Type.(type) { - case *types.Named: - if typeName := t.Obj(); typeName != nil { - if pkg := typeName.Pkg(); pkg != nil { - if typeName.Name() == "Verbose" && pkg.Path() == "k8s.io/klog/v2" { - return true - } - } - } - } - } - return false -} - -// isKlog checks whether an expression is klog.Verbose or the klog package itself. -func isKlog(expr ast.Expr, pass *analysis.Pass) bool { - // For klog.V(1) and klogV := klog.V(1) we can decide based on the type. - if isKlogVerbose(expr, pass) { - return true - } - - // In "klog.Info", "klog" is a package identifier. It doesn't need to - // be "klog" because here we look up the actual package. - return isPackage(expr, "k8s.io/klog/v2", pass) -} - -// isPackage checks whether an expression is an identifier that refers -// to a specific package like k8s.io/klog/v2. -func isPackage(expr ast.Expr, packagePath string, pass *analysis.Pass) bool { - if ident, ok := expr.(*ast.Ident); ok { - if object, ok := pass.TypesInfo.Uses[ident]; ok { - switch object := object.(type) { - case *types.PkgName: - pkg := object.Imported() - if pkg.Path() == packagePath { - return true - } - } - } - } - - return false -} - -// isGoLogger checks whether an expression is logr.Logger. -func isGoLogger(expr ast.Expr, pass *analysis.Pass) bool { - if typeAndValue, ok := pass.TypesInfo.Types[expr]; ok { - switch t := typeAndValue.Type.(type) { - case *types.Named: - if typeName := t.Obj(); typeName != nil { - if pkg := typeName.Pkg(); pkg != nil { - if typeName.Name() == "Logger" && pkg.Path() == "github.com/go-logr/logr" { - return true - } - } - } - } - } - return false -} - -func isUnstructured(fName string) bool { - // List of klog functions we do not want to use after migration to structured logging. - unstrucured := []string{ - "Infof", "Info", "Infoln", "InfoDepth", - "Warning", "Warningf", "Warningln", "WarningDepth", - "Error", "Errorf", "Errorln", "ErrorDepth", - "Fatal", "Fatalf", "Fatalln", "FatalDepth", - "Exit", "Exitf", "Exitln", "ExitDepth", - } - - for _, name := range unstrucured { - if fName == name { - return true - } - } - - return false -} - -func isContextualCall(fName string) bool { - // List of klog functions we still want to use after migration to - // contextual logging. This is an allow list, so any new acceptable - // klog call has to be added here. - contextual := []string{ - "Background", - "ClearLogger", - "ContextualLogger", - "EnableContextualLogging", - "FlushAndExit", - "FlushLogger", - "FromContext", - "KObj", - "KObjs", - "KRef", - "LoggerWithName", - "LoggerWithValues", - "NewContext", - "SetLogger", - "SetLoggerWithOptions", - "StartFlushDaemon", - "StopFlushDaemon", - "TODO", - } - for _, name := range contextual { - if fName == name { - return true - } - } - - return false -} - -// isKeysValid check if all keys in keyAndValues is string type -func isKeysValid(keyValues []ast.Expr, fun ast.Expr, pass *analysis.Pass, funName string) { - if len(keyValues)%2 != 0 { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Additional arguments to %s should always be Key Value pairs. Please check if there is any key or value missing.", funName), - }) - return - } - - for index, arg := range keyValues { - if index%2 != 0 { - continue - } - lit, ok := arg.(*ast.BasicLit) - if !ok { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value.", arg), - }) - continue - } - if lit.Kind != token.STRING { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments are expected to be inlined constant strings. Please replace %v provided with string value.", lit.Value), - }) - continue - } - isASCII := utf8string.NewString(lit.Value).IsASCII() - if !isASCII { - pass.Report(analysis.Diagnostic{ - Pos: fun.Pos(), - Message: fmt.Sprintf("Key positional arguments %s are expected to be lowerCamelCase alphanumeric strings. Please remove any non-Latin characters.", lit.Value), - }) - } - } -} - -func checkForFormatSpecifier(expr *ast.CallExpr, pass *analysis.Pass) bool { - if selExpr, ok := expr.Fun.(*ast.SelectorExpr); ok { - // extracting function Name like Infof - fName := selExpr.Sel.Name - if strings.HasSuffix(fName, "f") { - // Allowed for calls like Infof. - return false - } - if specifier, found := hasFormatSpecifier(expr.Args); found { - msg := fmt.Sprintf("logging function %q should not use format specifier %q", fName, specifier) - pass.Report(analysis.Diagnostic{ - Pos: expr.Fun.Pos(), - Message: msg, - }) - return true - } - } - return false -} - -func hasFormatSpecifier(fArgs []ast.Expr) (string, bool) { - formatSpecifiers := []string{ - "%v", "%+v", "%#v", "%T", - "%t", "%b", "%c", "%d", "%o", "%O", "%q", "%x", "%X", "%U", - "%e", "%E", "%f", "%F", "%g", "%G", "%s", "%q", "%p", - } - for _, fArg := range fArgs { - if arg, ok := fArg.(*ast.BasicLit); ok { - for _, specifier := range formatSpecifiers { - if strings.Contains(arg.Value, specifier) { - return specifier, true - } - } - } - } - return "", false -} - -// checkForContextAndLogger ensures that a function doesn't accept both a -// context and a logger. That is problematic because it leads to ambiguity: -// does the context already contain the logger? That matters when passing it on -// without the logger. -func checkForContextAndLogger(n ast.Node, params *ast.FieldList, pass *analysis.Pass, c *config) { - var haveLogger, haveContext bool - - for _, param := range params.List { - if typeAndValue, ok := pass.TypesInfo.Types[param.Type]; ok { - switch t := typeAndValue.Type.(type) { - case *types.Named: - if typeName := t.Obj(); typeName != nil { - if pkg := typeName.Pkg(); pkg != nil { - if typeName.Name() == "Logger" && pkg.Path() == "github.com/go-logr/logr" { - haveLogger = true - } else if typeName.Name() == "Context" && pkg.Path() == "context" { - haveContext = true - } - } - } - } - } - } - - if haveLogger && haveContext { - pass.Report(analysis.Diagnostic{ - Pos: n.Pos(), - End: n.End(), - Message: `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.`, - }) - } -} - -// checkForIfEnabled detects `if klog.V(..).Enabled() { ...` and `if -// logger.V(...).Enabled()` and suggests capturing the result of V. -func checkForIfEnabled(i *ast.IfStmt, pass *analysis.Pass, c *config) { - // if i.Init == nil { - // A more complex if statement, let's assume it's okay. - // return - // } - - // Must be a method call. - callExpr, ok := i.Cond.(*ast.CallExpr) - if !ok { - return - } - selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) - if !ok { - return - } - - // We only care about calls to Enabled(). - if selExpr.Sel.Name != "Enabled" { - return - } - - // And it must be Enabled for klog or logr.Logger. - if !isKlogVerbose(selExpr.X, pass) && - !isGoLogger(selExpr.X, pass) { - return - } - - // logger.Enabled() is okay, logger.V(1).Enabled() is not. - // That means we need to check for another selector expression - // with V as method name. - subCallExpr, ok := selExpr.X.(*ast.CallExpr) - if !ok { - return - } - subSelExpr, ok := subCallExpr.Fun.(*ast.SelectorExpr) - if !ok || subSelExpr.Sel.Name != "V" { - return - } - - // klogV is recommended as replacement for klog.V(). For logr.Logger - // let's use the root of the selector, which should be a variable. - varName := "klogV" - funcCall := "klog.V" - if isGoLogger(subSelExpr.X, pass) { - varName = "logger" - root := subSelExpr - for s, ok := root.X.(*ast.SelectorExpr); ok; s, ok = root.X.(*ast.SelectorExpr) { - root = s - } - if id, ok := root.X.(*ast.Ident); ok { - varName = id.Name - } - funcCall = varName + ".V" - } - - pass.Report(analysis.Diagnostic{ - Pos: i.Pos(), - End: i.End(), - Message: fmt.Sprintf("the result of %s should be stored in a variable and then be used multiple times: if %s := %s(); %s.Enabled() { ... %s.Info ... }", - funcCall, varName, funcCall, varName, varName), - }) -} diff --git a/hack/tools/logcheck/plugin/plugin.go b/hack/tools/logcheck/plugin/plugin.go deleted file mode 100644 index cd3e04ce9..000000000 --- a/hack/tools/logcheck/plugin/plugin.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -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 main is meant to be compiled as a plugin for golangci-lint, see -// https://golangci-lint.run/contributing/new-linters/#create-a-plugin. -package main - -import ( - "golang.org/x/tools/go/analysis" - "k8s.io/klog/hack/tools/logcheck/pkg" -) - -type analyzerPlugin struct{} - -func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { - return []*analysis.Analyzer{ - pkg.Analyser(), - } -} - -// AnalyzerPlugin is the entry point for golangci-lint. -var AnalyzerPlugin analyzerPlugin diff --git a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go deleted file mode 100644 index 045eb8d10..000000000 --- a/hack/tools/logcheck/testdata/src/allowUnstructuredLogs/allowUnstructuredLogs.go +++ /dev/null @@ -1,67 +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. -*/ - -// 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 `logging function "InfoS" should not use format specifier "%s"` - klog.ErrorS(nil, "test no.: %d", 1) // want `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") - 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/contextual/contextual.go b/hack/tools/logcheck/testdata/src/contextual/contextual.go deleted file mode 100644 index 51053f6e6..000000000 --- a/hack/tools/logcheck/testdata/src/contextual/contextual.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -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 contextual - -import ( - "context" - - "github.com/go-logr/logr" -) - -type myFuncType func(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` - -func usingMyFuncType(firstParam int, - callback myFuncType, // Will be warned about at the type definition, not here. - lastParam int) { -} - -func usingInlineFunc(firstParam int, - callback func(ctx context.Context, logger logr.Logger, msg string), // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` - lastParam int) { -} - -type myStruct struct { - myFuncField func(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` -} - -func (m myStruct) myMethod(ctx context.Context, logger logr.Logger, msg string) { // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` -} - -func myFunction(ctx context.Context, logger logr.Logger, msg string) { // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` -} - -type myInterface interface { - myInterfaceMethod(ctx context.Context, logger logr.Logger, msg string) // want `A function should accept either a context or a logger, but not both. Having both makes calling the function harder because it must be defined whether the context must contain the logger and callers have to follow that.` -} diff --git a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go deleted file mode 100644 index f03e0e989..000000000 --- a/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go +++ /dev/null @@ -1,67 +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. -*/ - -// 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 doNotAllowUnstructuredLogs - -import ( - klog "k8s.io/klog/v2" -) - -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 `logging function "InfoS" should not use format specifier "%s"` - klog.ErrorS(nil, "test no.: %d", 1) // want `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` - klog.Infoln("test log") // want `unstructured logging function "Infoln" should not be used` - klog.InfoDepth(1, "test log") // want `unstructured logging function "InfoDepth" should not be used` - klog.Warning("test log") // want `unstructured logging function "Warning" should not be used` - klog.Warningf("test log") // want `unstructured logging function "Warningf" should not be used` - klog.WarningDepth(1, "test log") // want `unstructured logging function "WarningDepth" should not be used` - klog.Error("test log") // want `unstructured logging function "Error" should not be used` - klog.Errorf("test log") // want `unstructured logging function "Errorf" should not be used` - klog.Errorln("test log") // want `unstructured logging function "Errorln" should not be used` - klog.ErrorDepth(1, "test log") // want `unstructured logging function "ErrorDepth" should not be used` - klog.Fatal("test log") // want `unstructured logging function "Fatal" should not be used` - 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/github.com/go-logr/logr/logr.go b/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go deleted file mode 100644 index fa2da1e9d..000000000 --- a/hack/tools/logcheck/testdata/src/github.com/go-logr/logr/logr.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 logr provides empty stubs for github.com/go-logr/logr for testing -// with golang.org/x/tools/go/analysis/analysistest. -package logr - -import ( - "context" -) - -type Logger struct{} - -func (l Logger) Enabled() bool { return false } -func (l Logger) WithName(name string) Logger { return l } -func (l Logger) WithValues(kv ...interface{}) Logger { return l } -func (l Logger) V(level int) Logger { return l } -func (l Logger) Info(msg string, kv ...interface{}) {} -func (l Logger) Error(err error, msg string, kv ...interface{}) {} - -func NewContext(ctx context.Context, logger Logger) context.Context { - return nil -} diff --git a/hack/tools/logcheck/testdata/src/gologr/gologr.go b/hack/tools/logcheck/testdata/src/gologr/gologr.go deleted file mode 100644 index 6d533bb1b..000000000 --- a/hack/tools/logcheck/testdata/src/gologr/gologr.go +++ /dev/null @@ -1,38 +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. -*/ - -// 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 gologr - -import ( - "github.com/go-logr/logr" -) - -var logger logr.Logger - -func logging() { - logger.Info("hello", "missing value") // want `Additional arguments to Info should always be Key Value pairs. Please check if there is any key or value missing.` - logger.Error(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` - logger.WithValues("missing value") // want `Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing.` - - logger.V(1).Info("hello", "missing value") // want `Additional arguments to Info should always be Key Value pairs. Please check if there is any key or value missing.` - logger.V(1).Error(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` - logger.V(1).WithValues("missing value") // want `Additional arguments to WithValues should always be Key Value pairs. Please check if there is any key or value missing.` -} diff --git a/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go b/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go deleted file mode 100644 index 22d8bb84d..000000000 --- a/hack/tools/logcheck/testdata/src/helpers/doNotAllowDirectCalls.go +++ /dev/null @@ -1,43 +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. -*/ - -// 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 helpers - -import ( - "context" - - "github.com/go-logr/logr" - klog "k8s.io/klog/v2" -) - -var logger klog.Logger - -func doNotAlllowDirectCalls() { - logger.WithName("foo") // want `function "WithName" should be called through klogr.LoggerWithName` - logger.WithValues("a", "b") // want `function "WithValues" should be called through klogr.LoggerWithValues` - logr.NewContext(context.Background(), logger) // want `function "NewContext" should be called through klogr.NewContext` -} - -func allowHelpers() { - klog.LoggerWithName(logger, "foo") - klog.LoggerWithValues(logger, "a", "b") - klog.NewContext(context.Background(), logger) -} diff --git a/hack/tools/logcheck/testdata/src/importrename/importrename.go b/hack/tools/logcheck/testdata/src/importrename/importrename.go deleted file mode 100644 index b5382bc60..000000000 --- a/hack/tools/logcheck/testdata/src/importrename/importrename.go +++ /dev/null @@ -1,30 +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. -*/ - -// 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 importrename - -import ( - glog "k8s.io/klog/v2" -) - -func dontAllowUnstructuredLogs() { - glog.Info("test log") // want `unstructured logging function "Info" 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 deleted file mode 100644 index d62a5d433..000000000 --- a/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go +++ /dev/null @@ -1,250 +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. -*/ - -// This fake package is created as package golang.org/x/tools/go/analysis/analysistest -// expects test data dependency to be testdata/src - -package v2 - -import ( - "context" - - "github.com/go-logr/logr" -) - -// Verbose is a boolean type that implements Infof (like Printf) etc. -// See the documentation of V for more information. -type Verbose struct { - enabled bool -} - -type Level int32 - -type Logger = logr.Logger - -func V(level Level) Verbose { - - return Verbose{enabled: false} -} - -// Enabled returns true if logging at the selected level is enabled. -func (v Verbose) Enabled() bool { - return false -} - -// Info is equivalent to the global Info function, guarded by the value of v. -// See the documentation of V for usage. -func (v Verbose) Info(args ...interface{}) { - -} - -// 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{}) { - -} - -// 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{}) { - -} - -// 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{}) { - -} - -func InfoSDepth(depth int, msg string, keysAndValues ...interface{}) { -} - -// Deprecated: Use ErrorS instead. -func (v Verbose) Error(err error, msg string, args ...interface{}) { - -} - -// ErrorS is equivalent to the global Error function, guarded by the value of v. -// See the documentation of V for usage. -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{}) { -} - -// 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{}) { -} - -// Infoln logs to the INFO log. -// Arguments are handled in the manner of fmt.Println; a newline is always appended. -func Infoln(args ...interface{}) { -} - -// 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{}) { -} - -// InfoS structured logs to the INFO log. -// The msg argument used to add constant description to the log line. -// The key/value pairs would be join by "=" ; a newline is always appended. -// -// Basic examples: -// >> klog.InfoS("Pod status updated", "pod", "kubedns", "status", "ready") -// output: -// >> I1025 00:15:15.525108 1 controller_utils.go:116] "Pod status updated" pod="kubedns" status="ready" -func InfoS(msg string, keysAndValues ...interface{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// ErrorS structured logs to the ERROR, WARNING, and INFO logs. -// the err argument used as "err" field of log line. -// The msg argument used to add constant description to the log line. -// The key/value pairs would be join by "=" ; a newline is always appended. -// -// Basic examples: -// >> klog.ErrorS(err, "Failed to update pod status") -// output: -// >> E1025 00:15:15.525108 1 controller_utils.go:114] "Failed to update pod status" err="timeout" -func ErrorS(err error, msg string, keysAndValues ...interface{}) { -} - -// ErrorSDepth acts as ErrorS but uses depth to determine which call frame to log. -// ErrorSDepth(0, "msg") is the same as ErrorS("msg"). -func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) { -} - -// Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls os.Exit(255). -// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. -func Fatal(args ...interface{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -// 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{}) { -} - -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{}) { -} - -// KObj emulates klog.KObj -func KObj(obj interface{}) interface{} { - return nil -} - -// KObjs emulates klog.KObjs -func KObjs(obj interface{}) interface{} { - return nil -} - -func KRef(namespace, name string) interface{} { - return nil -} - -func FromContext(ctx context.Context) Logger { - return Logger{} -} - -func NewContext(ctx context.Context, logger Logger) context.Context { - return ctx -} - -func LoggerWithName(logger Logger, name string) Logger { - return Logger{} -} - -func LoggerWithValues(logger Logger, kvs ...interface{}) Logger { - return Logger{} -} - -func TODO() Logger { - return Logger{} -} - -func Background() Logger { - return Logger{} -} diff --git a/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go deleted file mode 100644 index 6215b2476..000000000 --- a/hack/tools/logcheck/testdata/src/mixed/allowUnstructuredLogs.go +++ /dev/null @@ -1,31 +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. -*/ - -// 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 mixed - -import ( - klog "k8s.io/klog/v2" -) - -func allowUnstructuredLogs() { - // Error is not expected as this file allows unstructured logging - klog.Infof("test log") -} diff --git a/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go deleted file mode 100644 index bd448e97c..000000000 --- a/hack/tools/logcheck/testdata/src/mixed/doNotAllowUnstructuredLogs.go +++ /dev/null @@ -1,30 +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. -*/ - -// 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 mixed - -import ( - klog "k8s.io/klog/v2" -) - -func dontAllowUnstructuredLogs() { - klog.Info("test log") // want `unstructured logging function "Info" should not be used` -} diff --git a/hack/tools/logcheck/testdata/src/mixed/structured_logging b/hack/tools/logcheck/testdata/src/mixed/structured_logging deleted file mode 100644 index 050a12714..000000000 --- a/hack/tools/logcheck/testdata/src/mixed/structured_logging +++ /dev/null @@ -1,6 +0,0 @@ -# This file contains regular expressions that are matched against /, -# for example k8s.io/cmd/kube-scheduler/app/config/config.go. -# -# Any file that is matched may only use structured logging calls. - -structured .*doNotAllowUnstructuredLogs.go diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging b/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging deleted file mode 100644 index d298e40aa..000000000 --- a/hack/tools/logcheck/testdata/src/onlyAllowContextual/klog_logging +++ /dev/null @@ -1 +0,0 @@ -contextual .*onlyAllowContextual.go diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go b/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go deleted file mode 100644 index 4cc5e128e..000000000 --- a/hack/tools/logcheck/testdata/src/onlyAllowContextual/onlyAllowContextual.go +++ /dev/null @@ -1,32 +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. -*/ - -// 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 onlyallowcontextual - -import ( - klog "k8s.io/klog/v2" -) - -func doNotAlllowKlog() { - klog.InfoS("test log") // want `function "InfoS" should not be used, convert to contextual logging` - klog.ErrorS(nil, "test log") // want `function "ErrorS" should not be used, convert to contextual logging` - klog.V(1).Infof("test log") // want `function "V" should not be used, convert to contextual logging` `function "Infof" should not be used, convert to contextual logging` -} diff --git a/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go b/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go deleted file mode 100644 index 07bb2a2a9..000000000 --- a/hack/tools/logcheck/testdata/src/onlyAllowContextual/whitelistedKlog.go +++ /dev/null @@ -1,37 +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. -*/ - -// 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 onlyallowcontextual - -import ( - klog "k8s.io/klog/v2" -) - -func allowKlog() { - klog.KObj(nil) - klog.KObjs(nil) - klog.KRef("", "") - klog.FromContext(nil) - klog.TODO() - klog.Background() - klog.LoggerWithName(klog.Logger{}, "foo") - klog.LoggerWithValues(klog.Logger{}, "a", "b") -} diff --git a/hack/tools/logcheck/testdata/src/parameters/parameters.go b/hack/tools/logcheck/testdata/src/parameters/parameters.go deleted file mode 100644 index 919870f1e..000000000 --- a/hack/tools/logcheck/testdata/src/parameters/parameters.go +++ /dev/null @@ -1,55 +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. -*/ - -// 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 parameter -// checking. - -package parameters - -import ( - "fmt" - - klog "k8s.io/klog/v2" -) - -func parameterCalls() { - // format strings (incomplete list...) - klog.Infof("%d", 1) - klog.InfoS("%d", 1) // want `logging function "InfoS" should not use format specifier "%d"` - klog.Info("%d", 1) // want `logging function "Info" should not use format specifier "%d"` - klog.Infoln("%d", 1) // want `logging function "Infoln" should not use format specifier "%d"` - klog.V(1).Infof("%d", 1) - klog.V(1).InfoS("%d", 1) // want `logging function "InfoS" should not use format specifier "%d"` - klog.V(1).Info("%d", 1) // want `logging function "Info" should not use format specifier "%d"` - klog.V(1).Infoln("%d", 1) // want `logging function "Infoln" should not use format specifier "%d"` - klog.Errorf("%d", 1) - klog.ErrorS(nil, "%d", 1) // want `logging function "ErrorS" should not use format specifier "%d"` - klog.Error("%d", 1) // want `logging function "Error" should not use format specifier "%d"` - klog.Errorln("%d", 1) // want `logging function "Errorln" should not use format specifier "%d"` - - klog.InfoS("hello", "value", fmt.Sprintf("%d", 1)) - - // odd number of parameters - klog.InfoS("hello", "key") // want `Additional arguments to InfoS should always be Key Value pairs. Please check if there is any key or value missing.` - klog.ErrorS(nil, "hello", "key") // want `Additional arguments to ErrorS should always be Key Value pairs. Please check if there is any key or value missing.` - - // non-string keys - klog.InfoS("hello", "1", 2) - klog.InfoS("hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` - klog.ErrorS(nil, "hello", "1", 2) - klog.ErrorS(nil, "hello", 1, 2) // want `Key positional arguments are expected to be inlined constant strings. Please replace 1 provided with string value` -} diff --git a/hack/tools/logcheck/testdata/src/verbose/verbose.go b/hack/tools/logcheck/testdata/src/verbose/verbose.go deleted file mode 100644 index fa8758c0b..000000000 --- a/hack/tools/logcheck/testdata/src/verbose/verbose.go +++ /dev/null @@ -1,57 +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. -*/ - -// 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 verbose - -import ( - "github.com/go-logr/logr" - klog "k8s.io/klog/v2" -) - -var l, logger logr.Logger - -func verboseLogging() { - klog.V(1).Info("test log") // want `unstructured logging function "Info" should not be used` - if klogV := klog.V(1); klogV.Enabled() { - klogV.Infof("hello %s", "world") // want `unstructured logging function "Infof" should not be used` - } - - // \(\) is actually () in the diagnostic output. We have to escape here - // because `want` expects a regular expression. - - if klog.V(1).Enabled() { // want `the result of klog.V should be stored in a variable and then be used multiple times: if klogV := klog.V\(\); klogV.Enabled\(\) { ... klogV.Info ... }` - klog.V(1).InfoS("I'm logging at level 1.") - } - - if l.V(1).Enabled() { // want `the result of l.V should be stored in a variable and then be used multiple times: if l := l.V\(\); l.Enabled\(\) { ... l.Info ... }` - l.V(1).Info("I'm logging at level 1.") - } - - if l := l.V(2); l.Enabled() { - l.Info("I'm logging at level 2.") - } - - if l := logger.V(2); l.Enabled() { - // This is probably an error (should be l instead of logger), - // but not currently detected. - logger.Info("I wanted to log at level 2, but really it is 0.") - } -} From c5c3132f9ea3861d13a76db3040b1a2b567e2163 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 13 Jun 2022 20:53:49 +0200 Subject: [PATCH 089/125] klog.Fatal: document behavior and alternatives After reverting https://github.com/kubernetes/klog/pull/79 users who want the full dump of all goroutines can still get it by calling github.com/go-logr/lib.Backtrace(lib.BacktraceAll(true)). --- klog.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/klog.go b/klog.go index 1a11f7814..8305e2520 100644 --- a/klog.go +++ b/klog.go @@ -1589,7 +1589,18 @@ func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) } // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, -// including a stack trace of all running goroutines, then calls OsExit(255). +// prints stack trace(s), then calls OsExit(255). +// +// Stderr only receives a dump of the current goroutine's stack trace. Log files, +// if there are any, receive a dump of the stack traces in all goroutines. +// +// Callers who want more control over handling of fatal events may instead use a +// combination of different functions: +// - some info or error logging function, optionally with a stack trace +// value generated by github.com/go-logr/lib/dbg.Backtrace +// - Flush to flush pending log data +// - panic, os.Exit or returning to the caller with an error +// // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Fatal(args ...interface{}) { logging.print(severity.FatalLog, logging.logger, logging.filter, args...) From e4329d261ac4f3d712784832a201019bc4472efb Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Sun, 19 Jun 2022 00:34:47 +0530 Subject: [PATCH 090/125] GIT-331: fix shadowing key from the kv pair --- internal/serialize/keyvalues.go | 4 ++-- internal/serialize/keyvalues_test.go | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index cb97576bb..f85d7ccf8 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -109,10 +109,10 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { // 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 { + if sK, ok := k.(string); ok { // Avoid one allocation when the key is a string, which // normally it should be. - b.WriteString(k) + b.WriteString(sK) } else { b.WriteString(fmt.Sprintf("%s", k)) } diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index 79260092c..d4b338519 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -140,6 +140,18 @@ No whitespace.`, keysValues: []interface{}{"point-1", point{100, 200}, "point-2", emptyPoint}, want: " point-1=\"x=100, y=200\" point-2=\"\"", }, + { + keysValues: []interface{}{struct{ key string }{key: "k1"}, "value"}, + want: " {k1}=\"value\"", + }, + { + keysValues: []interface{}{1, "test"}, + want: " %!s(int=1)=\"test\"", + }, + { + keysValues: []interface{}{map[string]string{"k": "key"}, "value"}, + want: " map[k:key]=\"value\"", + }, } for _, d := range testKVList { From 325472db89c9891fc9ee848a951d5415f0b302be Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 10 Jun 2022 14:07:18 +0200 Subject: [PATCH 091/125] ktesting: capture log data in memory This is useful for tests that specifically want to check for expected log output in the code that they are testing. Capturing log data is done unconditionally. Having a configuration option for it would be possible, but then usage and code become more complicated for (probably) very little performance gains in tests that don't need the captured data. --- ktesting/example_test.go | 71 +++++++++++++ ktesting/testinglogger.go | 207 +++++++++++++++++++++++++++++++++++++- 2 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 ktesting/example_test.go diff --git a/ktesting/example_test.go b/ktesting/example_test.go new file mode 100644 index 000000000..c2fa390c5 --- /dev/null +++ b/ktesting/example_test.go @@ -0,0 +1,71 @@ +/* +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 ktesting_test + +import ( + "errors" + "fmt" + "time" + + "k8s.io/klog/v2/ktesting" +) + +func ExampleUnderlier() { + logger := ktesting.NewLogger(ktesting.NopTL{}, ktesting.NewConfig(ktesting.Verbosity(4))) + + logger.Error(errors.New("failure"), "I failed", "what", "something") + logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world") + logger.WithValues("request", 42, "anotherValue", "fish").Info("hello world 2", "yetAnotherValue", "thanks") + logger.WithName("example").Info("with name") + logger.V(4).Info("higher verbosity") + logger.V(5).Info("Not captured because of ktesting.Verbosity(4) above. Normally it would be captured because default verbosity is 5.") + + testingLogger, ok := logger.GetSink().(ktesting.Underlier) + if !ok { + panic("Should have had a ktesting LogSink!?") + } + + t := testingLogger.GetUnderlying() + t.Log("This goes to /dev/null...") + + buffer := testingLogger.GetBuffer() + fmt.Printf("%s\n", buffer.String()) + + log := buffer.Data() + for i, entry := range log { + if i > 0 && + entry.Timestamp.Sub(log[i-1].Timestamp).Nanoseconds() < 0 { + fmt.Printf("Unexpected timestamp order: #%d %s > #%d %s", i-1, log[i-1].Timestamp, i, entry.Timestamp) + } + // Strip varying time stamp before dumping the struct. + entry.Timestamp = time.Time{} + fmt.Printf("log entry #%d: %+v\n", i, entry) + } + + // Output: + // ERROR I failed err="failure" what="something" + // INFO hello world request=42 anotherValue="fish" + // INFO hello world 2 request=42 anotherValue="fish" yetAnotherValue="thanks" + // INFO example: with name + // INFO higher verbosity + // + // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something]} + // log entry #1: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[]} + // log entry #2: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world 2 Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[yetAnotherValue thanks]} + // log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err: WithKVList:[] ParameterKVList:[]} + // log entry #4: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:higher verbosity Verbosity:4 Err: WithKVList:[] ParameterKVList:[]} +} diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index d796f60aa..905c1d634 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -20,6 +20,18 @@ limitations under the License. // Therefore it can be used in standard Go tests and Gingko test suites // to ensure that output is associated with the currently running test. // +// In addition, the log data is captured in a buffer and can be used by the +// test to verify that the code under test is logging as expected. To get +// access to that data, cast the LogSink into the Underlier type and retrieve +// it: +// +// logger := ktesting.NewLogger(...) +// if testingLogger, ok := logger.GetSink().(ktesting.Underlier); ok { +// t := testingLogger.GetUnderlying() +// buffer := testingLogger.GetBuffer() +// text := buffer.String() +// log := buffer.Data() +// // Serialization of the structured log parameters is done in the same way // as for klog.InfoS. // @@ -31,6 +43,9 @@ package ktesting import ( "bytes" + "strings" + "sync" + "time" "github.com/go-logr/logr" @@ -49,6 +64,20 @@ type TL interface { Log(args ...interface{}) } +// NopTL implements TL with empty stubs. It can be used when only capturing +// output in memory is relevant. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type NopTL struct{} + +func (n NopTL) Helper() {} +func (n NopTL) Log(args ...interface{}) {} + +var _TL = NopTL{} + // NewLogger constructs a new logger for the given test interface. // // Experimental @@ -61,14 +90,149 @@ func NewLogger(t TL, c *Config) logr.Logger { prefix: "", values: nil, config: c, + buffer: new(buffer), }) } +// Buffer stores log entries as formatted text and structured data. +// It is safe to use this concurrently. +// +// Experimental +// +// Notice: This interface is EXPERIMENTAL and may be changed or removed in a +// later release. +type Buffer interface { + // String returns the log entries in a format that is similar to the + // klog text output. + String() string + + // Data returns the log entries as structs. + Data() Log +} + +// Log contains log entries in the order in which they were generated. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type Log []LogEntry + +// DeepCopy returns a copy of the log. The error instance and key/value +// pairs remain shared. +// +// Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func (l Log) DeepCopy() Log { + log := make(Log, 0, len(l)) + log = append(log, l...) + return log +} + +// LogEntry represents all information captured for a log entry. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type LogEntry struct { + // Timestamp stores the time when the log entry was created. + Timestamp time.Time + + // Type is either LogInfo or LogError. + Type LogType + + // Prefix contains the WithName strings concatenated with a slash. + Prefix string + + // Message is the fixed log message string. + Message string + + // Verbosity is always 0 for LogError. + Verbosity int + + // Err is always nil for LogInfo. It may or may not be + // nil for LogError. + Err error + + // WithKVList are the concatenated key/value pairs from WithValues + // calls. It's guaranteed to have an even number of entries because + // the logger ensures that when WithValues is called. + WithKVList []interface{} + + // ParameterKVList are the key/value pairs passed into the call, + // without any validation. + ParameterKVList []interface{} +} + +// LogType determines whether a log entry was created with an Error or Info +// call. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type LogType string + +const ( + // LogError is the special value used for Error log entries. + // + // Experimental + // + // Notice: This value is EXPERIMENTAL and may be changed or removed in + // a later release. + LogError = LogType("ERROR") + + // LogInfo is the special value used for Info log entries. + // + // Experimental + // + // Notice: This value is EXPERIMENTAL and may be changed or removed in + // a later release. + LogInfo = LogType("INFO") +) + +// Underlier is implemented by the LogSink of this logger. It provides access +// to additional APIs that are normally hidden behind the Logger API. +// +// Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type Underlier interface { + // GetUnderlying returns the testing instance that logging goes to. + GetUnderlying() TL + + // GetBuffer grants access to the in-memory copy of the log entries. + GetBuffer() Buffer +} + +type buffer struct { + mutex sync.Mutex + text strings.Builder + log Log +} + +func (b *buffer) String() string { + b.mutex.Lock() + defer b.mutex.Unlock() + return b.text.String() +} + +func (b *buffer) Data() Log { + b.mutex.Lock() + defer b.mutex.Unlock() + return b.log.DeepCopy() +} + type tlogger struct { t TL prefix string values []interface{} config *Config + buffer *buffer } func (l *tlogger) Init(info logr.RuntimeInfo) { @@ -83,7 +247,7 @@ func (l *tlogger) Info(level int, msg string, kvList ...interface{}) { buffer := &bytes.Buffer{} merged := serialize.MergeKVs(l.values, kvList) serialize.KVListFormat(buffer, merged...) - l.log("INFO", msg, buffer) + l.log(LogInfo, msg, level, buffer, nil, kvList) } func (l *tlogger) Enabled(level int) bool { @@ -98,10 +262,10 @@ func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { } merged := serialize.MergeKVs(l.values, kvList) serialize.KVListFormat(buffer, merged...) - l.log("ERROR", msg, buffer) + l.log(LogError, msg, 0, buffer, err, kvList) } -func (l *tlogger) log(what, msg string, buffer *bytes.Buffer) { +func (l *tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, err error, kvList []interface{}) { l.t.Helper() args := []interface{}{what} if l.prefix != "" { @@ -113,6 +277,34 @@ func (l *tlogger) log(what, msg string, buffer *bytes.Buffer) { args = append(args, string(buffer.Bytes()[1:])) } l.t.Log(args...) + + l.buffer.mutex.Lock() + defer l.buffer.mutex.Unlock() + + // Store as text. + l.buffer.text.WriteString(string(what)) + for i := 1; i < len(args); i++ { + l.buffer.text.WriteByte(' ') + l.buffer.text.WriteString(args[i].(string)) + } + lastArg := args[len(args)-1].(string) + if lastArg[len(lastArg)-1] != '\n' { + l.buffer.text.WriteByte('\n') + } + + // Store as raw data. + l.buffer.log = append(l.buffer.log, + LogEntry{ + Timestamp: time.Now(), + Type: what, + Prefix: l.prefix, + Message: msg, + Verbosity: level, + Err: err, + WithKVList: l.values, + ParameterKVList: kvList, + }, + ) } // WithName returns a new logr.Logger with the specified name appended. klogr @@ -133,5 +325,14 @@ func (l *tlogger) WithValues(kvList ...interface{}) logr.LogSink { return &new } +func (l *tlogger) GetUnderlying() TL { + return l.t +} + +func (l *tlogger) GetBuffer() Buffer { + return l.buffer +} + var _ logr.LogSink = &tlogger{} var _ logr.CallStackHelperLogSink = &tlogger{} +var _ Underlier = &tlogger{} From 280fc18291b4a0c146c8e94caccd282880da506b Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Mon, 20 Jun 2022 23:32:13 +0530 Subject: [PATCH 092/125] GIT-275: add tests for int and struct keys GIT-275: add map keys for example GIT-275: add map keys for example GIT-275: add benchmarking examples GIT-275: enable tests on go1.18 --- .github/workflows/test.yml | 2 +- examples/benchmarks/benchmarks_test.go | 27 ++++++++++++++++++++++++++ examples/output_test/output_test.go | 26 ++++++++++++++++++++++++- test/output.go | 21 ++++++++++++++++++++ test/zapr.go | 27 ++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 371bd29fd..11aecceb8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.15, 1.16, 1.17] + go-version: [1.15, 1.16, 1.17, 1.18] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/examples/benchmarks/benchmarks_test.go b/examples/benchmarks/benchmarks_test.go index 3105abafe..a343efe08 100644 --- a/examples/benchmarks/benchmarks_test.go +++ b/examples/benchmarks/benchmarks_test.go @@ -91,6 +91,33 @@ func BenchmarkOutput(b *testing.B) { "fail-verbosity-check": func(value interface{}) { klog.V(verbosityThreshold+1).InfoS("test", "key", value) }, + "non-standard-int-key-check": func(value interface{}) { + klog.InfoS("test", 1, value) + }, + "non-standard-struct-key-check": func(value interface{}) { + klog.InfoS("test", struct{ key string }{"test"}, value) + }, + "non-standard-map-key-check": func(value interface{}) { + klog.InfoS("test", map[string]bool{"key": true}, value) + }, + "pass-verbosity-non-standard-int-key-check": func(value interface{}) { + klog.V(verbosityThreshold).InfoS("test", 1, value) + }, + "pass-verbosity-non-standard-struct-key-check": func(value interface{}) { + klog.V(verbosityThreshold).InfoS("test", struct{ key string }{"test"}, value) + }, + "pass-verbosity-non-standard-map-key-check": func(value interface{}) { + klog.V(verbosityThreshold).InfoS("test", map[string]bool{"key": true}, value) + }, + "fail-verbosity-non-standard-int-key-check": func(value interface{}) { + klog.V(verbosityThreshold+1).InfoS("test", 1, value) + }, + "fail-verbosity-non-standard-struct-key-check": func(value interface{}) { + klog.V(verbosityThreshold+1).InfoS("test", struct{ key string }{"test"}, value) + }, + "fail-verbosity-non-standard-map-key-check": func(value interface{}) { + klog.V(verbosityThreshold+1).InfoS("test", map[string]bool{"key": true}, value) + }, } for name, config := range configs { diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index b41701217..ac10f92f8 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -63,7 +63,7 @@ func TestTextloggerOutput(t *testing.T) { }) } -// TestTextloggerOutput tests the zapr, directly and as backend. +// TestZaprOutput 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) @@ -121,6 +121,18 @@ func TestKlogrStackZapr(t *testing.T) { `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)"} +`, + `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} +{"caller":"test/output.go:","msg":"integer keys","v":0} +`, + `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} +{"caller":"test/output.go:","msg":"struct keys","v":0} +`, + `I output.go:] "map keys" map[test:%!s(bool=true)]="test" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} +{"caller":"test/output.go:","msg":"map keys","v":0} `, } { mapping[key] = value @@ -172,6 +184,18 @@ func TestKlogrInternalStackZapr(t *testing.T) { `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)"} +`, + `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} +{"caller":"test/output.go:","msg":"integer keys","v":0} +`, + `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} +{"caller":"test/output.go:","msg":"struct keys","v":0} +`, + `I output.go:] "map keys" map[test:%!s(bool=true)]="test" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} +{"caller":"test/output.go:","msg":"map keys","v":0} `, } { mapping[key] = value diff --git a/test/output.go b/test/output.go index 673e40c56..775571f2f 100644 --- a/test/output.go +++ b/test/output.go @@ -365,6 +365,27 @@ I output.go:] "test" firstKey=1 secondKey=3 text: "marshaler recursion", values: []interface{}{"obj", recursiveMarshaler{}}, expectedOutput: `I output.go:] "marshaler recursion" obj={} +`, + }, + "handle integer keys": { + withValues: []interface{}{1, "value", 2, "value2"}, + text: "integer keys", + values: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`, + }, + "struct keys": { + withValues: []interface{}{struct{ name string }{"name"}, "value", "test", "other value"}, + text: "struct keys", + values: []interface{}{"key", "val"}, + expectedOutput: `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`, + }, + "map keys": { + withValues: []interface{}{}, + text: "map keys", + values: []interface{}{map[string]bool{"test": true}, "test"}, + expectedOutput: `I output.go:] "map keys" map[test:%!s(bool=true)]="test" `, }, } diff --git a/test/zapr.go b/test/zapr.go index 32a82e68c..427d1f78a 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -217,6 +217,21 @@ I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" // klog.V(1).InfoS `I output.go:] "hello" what="one world" `: `{"caller":"test/output.go:","msg":"hello","v":1,"what":"one world"} +`, + + `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} +{"caller":"test/output.go:","msg":"odd number of arguments passed as key-value pairs for logging","ignored key":"akey2"} +{"caller":"test/output.go:","msg":"integer keys","v":0,"akey":"avalue"} +`, + + `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} +{"caller":"test/output.go:","msg":"struct keys","v":0,"key":"val"} +`, + `I output.go:] "map keys" map[test:%!s(bool=true)]="test" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} +{"caller":"test/output.go:","msg":"map keys","v":0} `, } } @@ -291,6 +306,18 @@ I output.go:] "test" firstKey=1 secondKey=3 {"caller":"test/output.go:","msg":"test","v":0,"firstKey":1,"secondKey":2} {"caller":"test/output.go:","msg":"test","v":0,"firstKey":1} {"caller":"test/output.go:","msg":"test","v":0,"firstKey":1,"secondKey":3} +`, + `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":1} +{"caller":"test/output.go:","msg":"integer keys","v":0} +`, + `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{}} +{"caller":"test/output.go:","msg":"struct keys","v":0} +`, + `I output.go:] "map keys" map[test:%!s(bool=true)]="test" +`: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} +{"caller":"test/output.go:","msg":"map keys","v":0} `, } { mapping[key] = value From 66544b3eef6d0089cd2afc336d4e46c69d25232e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 29 Jun 2022 13:27:28 +0200 Subject: [PATCH 093/125] ktesting: use black box testing It makes it more obvious what calls to the ktesting API will look like in real code. --- ktesting/testinglogger_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index 6c7ae187d..c4efa4629 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -5,13 +5,15 @@ Copyright 2020 Intel Coporation. SPDX-License-Identifier: Apache-2.0 */ -package ktesting +package ktesting_test import ( "bytes" "errors" "fmt" "testing" + + "k8s.io/klog/v2/ktesting" ) func TestInfo(t *testing.T) { @@ -104,7 +106,7 @@ func TestInfo(t *testing.T) { for n, test := range tests { t.Run(n, func(t *testing.T) { var buffer logToBuf - klogr := NewLogger(&buffer, NewConfig()) + klogr := ktesting.NewLogger(&buffer, ktesting.NewConfig()) for _, name := range test.names { klogr = klogr.WithName(name) } @@ -125,7 +127,7 @@ func TestInfo(t *testing.T) { } func TestCallDepth(t *testing.T) { - logger := NewLogger(t, NewConfig()) + logger := ktesting.NewLogger(t, ktesting.NewConfig()) logger.Info("hello world") } From 78f38f681e8446d060e15bcd7e6daf008946a91e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 4 Jul 2022 14:56:40 +0200 Subject: [PATCH 094/125] ktesting: fix type assertion Due a missing space, _TL was a variable of type NopTL instead of asserting that NopTL implements the TL interface. --- ktesting/testinglogger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 905c1d634..cf9fbdfe6 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -76,7 +76,7 @@ type NopTL struct{} func (n NopTL) Helper() {} func (n NopTL) Log(args ...interface{}) {} -var _TL = NopTL{} +var _ TL = NopTL{} // NewLogger constructs a new logger for the given test interface. // From 9405f8edad47f2b2bef7caa3cea95c8278fbea44 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 29 Jun 2022 13:23:07 +0200 Subject: [PATCH 095/125] ktesting: stop using testing.T when test completes When testing.T.Log gets called after the test has completed, it panics. There's also a data race (https://github.com/kubernetes/kubernetes/issues/110854). Normally that should never happen because tests should ensure that all goroutines have stopped before returning. But sometimes it is not possible to do that. ktesting now automatically protects against that by registering a cleanup function and redirecting all future output into klog. --- internal/dbg/dbg.go | 42 +++++++++ klog.go | 26 +----- ktesting/testinglogger.go | 163 ++++++++++++++++++++++++--------- ktesting/testinglogger_test.go | 102 +++++++++++++++++++++ 4 files changed, 269 insertions(+), 64 deletions(-) create mode 100644 internal/dbg/dbg.go diff --git a/internal/dbg/dbg.go b/internal/dbg/dbg.go new file mode 100644 index 000000000..f27bd1447 --- /dev/null +++ b/internal/dbg/dbg.go @@ -0,0 +1,42 @@ +// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// +// Copyright 2013 Google Inc. All Rights Reserved. +// +// 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 dbg provides some helper code for call traces. +package dbg + +import ( + "runtime" +) + +// Stacks is a wrapper for runtime.Stack that attempts to recover the data for +// all goroutines or the calling one. +func Stacks(all bool) []byte { + // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. + n := 10000 + if all { + n = 100000 + } + var trace []byte + for i := 0; i < 5; i++ { + trace = make([]byte, n) + nbytes := runtime.Stack(trace, all) + if nbytes < len(trace) { + return trace[:nbytes] + } + n *= 2 + } + return trace +} diff --git a/klog.go b/klog.go index 8305e2520..b96f77525 100644 --- a/klog.go +++ b/klog.go @@ -96,6 +96,7 @@ import ( "k8s.io/klog/v2/internal/buffer" "k8s.io/klog/v2/internal/clock" + "k8s.io/klog/v2/internal/dbg" "k8s.io/klog/v2/internal/serialize" "k8s.io/klog/v2/internal/severity" ) @@ -859,7 +860,7 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf if l.traceLocation.isSet() { if l.traceLocation.match(file, line) { - buf.Write(stacks(false)) + buf.Write(dbg.Stacks(false)) } } data := buf.Bytes() @@ -928,11 +929,11 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf // If -logtostderr has been specified, the loop below will do that anyway // as the first stack in the full dump. if !l.toStderr { - os.Stderr.Write(stacks(false)) + os.Stderr.Write(dbg.Stacks(false)) } // Write the stack trace for all goroutines to the files. - trace := stacks(true) + trace := dbg.Stacks(true) logExitFunc = func(error) {} // If we get a write error, we'll still exit below. for log := severity.FatalLog; log >= severity.InfoLog; log-- { if f := l.file[log]; f != nil { // Can be nil if -logtostderr is set. @@ -952,25 +953,6 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf } } -// stacks is a wrapper for runtime.Stack that attempts to recover the data for all goroutines. -func stacks(all bool) []byte { - // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. - n := 10000 - if all { - n = 100000 - } - var trace []byte - for i := 0; i < 5; i++ { - trace = make([]byte, n) - nbytes := runtime.Stack(trace, all) - if nbytes < len(trace) { - return trace[:nbytes] - } - n *= 2 - } - return trace -} - // logExitFunc provides a simple mechanism to override the default behavior // of exiting on error. Used in testing and to guarantee we reach a required exit // for fatal logs. Instead, exit could be a function rather than a method but that diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index cf9fbdfe6..b3272845d 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -49,6 +49,8 @@ import ( "github.com/go-logr/logr" + "k8s.io/klog/v2" + "k8s.io/klog/v2/internal/dbg" "k8s.io/klog/v2/internal/serialize" "k8s.io/klog/v2/internal/verbosity" ) @@ -80,18 +82,36 @@ var _ TL = NopTL{} // NewLogger constructs a new logger for the given test interface. // +// Beware that testing.T does not support logging after the test that +// it was created for has completed. If a test leaks goroutines +// and those goroutines log something after test completion, +// that output will be printed via the global klog logger with +// ` leaked goroutine` as prefix. +// // Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. func NewLogger(t TL, c *Config) logr.Logger { - return logr.New(&tlogger{ - t: t, - prefix: "", - values: nil, - config: c, - buffer: new(buffer), - }) + l := tlogger{ + shared: &tloggerShared{ + t: t, + config: c, + }, + } + + type testCleanup interface { + Cleanup(func()) + Name() string + } + + // Stopping the logging is optional and only done (and required) + // for testing.T/B/F. + if tb, ok := t.(testCleanup); ok { + tb.Cleanup(l.shared.stop) + l.shared.testName = tb.Name() + } + return logr.New(l) } // Buffer stores log entries as formatted text and structured data. @@ -203,6 +223,7 @@ const ( // later release. type Underlier interface { // GetUnderlying returns the testing instance that logging goes to. + // It returns nil when the test has completed already. GetUnderlying() TL // GetBuffer grants access to the in-memory copy of the log entries. @@ -227,35 +248,95 @@ func (b *buffer) Data() Log { return b.log.DeepCopy() } +// tloggerShared holds values that are the same for all LogSink instances. It +// gets referenced by pointer in the tlogger struct. +type tloggerShared struct { + // mutex protects access to t. + mutex sync.Mutex + + // t gets cleared when the test is completed. + t TL + + // We warn once when a leaked goroutine is detected because + // it logs after test completion. + goroutineWarningDone bool + + testName string + config *Config + buffer buffer + callDepth int +} + +func (ls *tloggerShared) stop() { + ls.mutex.Lock() + defer ls.mutex.Unlock() + ls.t = nil +} + +// tlogger is the actual LogSink implementation. type tlogger struct { - t TL + shared *tloggerShared prefix string values []interface{} - config *Config - buffer *buffer } -func (l *tlogger) Init(info logr.RuntimeInfo) { +func (l tlogger) fallbackLogger() logr.Logger { + logger := klog.Background().WithValues(l.values...).WithName(l.shared.testName + " leaked goroutine") + if l.prefix != "" { + logger = logger.WithName(l.prefix) + } + // Skip direct caller (= Error or Info) plus the logr wrapper. + logger = logger.WithCallDepth(l.shared.callDepth + 1) + + if !l.shared.goroutineWarningDone { + logger.WithCallDepth(1).Error(nil, "WARNING: test kept at least one goroutine running after test completion", "callstack", string(dbg.Stacks(false))) + l.shared.goroutineWarningDone = true + } + return logger } -func (l *tlogger) GetCallStackHelper() func() { - return l.t.Helper +func (l tlogger) Init(info logr.RuntimeInfo) { + l.shared.callDepth = info.CallDepth } -func (l *tlogger) Info(level int, msg string, kvList ...interface{}) { - l.t.Helper() +func (l tlogger) GetCallStackHelper() func() { + l.shared.mutex.Lock() + defer l.shared.mutex.Unlock() + if l.shared.t == nil { + return func() {} + } + + return l.shared.t.Helper +} + +func (l tlogger) Info(level int, msg string, kvList ...interface{}) { + l.shared.mutex.Lock() + defer l.shared.mutex.Unlock() + if l.shared.t == nil { + l.fallbackLogger().V(level).Info(msg, kvList...) + return + } + + l.shared.t.Helper() buffer := &bytes.Buffer{} merged := serialize.MergeKVs(l.values, kvList) serialize.KVListFormat(buffer, merged...) l.log(LogInfo, msg, level, buffer, nil, kvList) } -func (l *tlogger) Enabled(level int) bool { - return l.config.vstate.Enabled(verbosity.Level(level), 1) +func (l tlogger) Enabled(level int) bool { + return l.shared.config.vstate.Enabled(verbosity.Level(level), 1) } -func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { - l.t.Helper() +func (l tlogger) Error(err error, msg string, kvList ...interface{}) { + l.shared.mutex.Lock() + defer l.shared.mutex.Unlock() + if l.shared.t == nil { + l.fallbackLogger().Error(err, msg, kvList...) + return + } + + l.shared.t.Helper() buffer := &bytes.Buffer{} if err != nil { serialize.KVListFormat(buffer, "err", err) @@ -265,8 +346,8 @@ func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { l.log(LogError, msg, 0, buffer, err, kvList) } -func (l *tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, err error, kvList []interface{}) { - l.t.Helper() +func (l tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, err error, kvList []interface{}) { + l.shared.t.Helper() args := []interface{}{what} if l.prefix != "" { args = append(args, l.prefix+":") @@ -276,24 +357,24 @@ func (l *tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, // Skip leading space inserted by serialize.KVListFormat. args = append(args, string(buffer.Bytes()[1:])) } - l.t.Log(args...) + l.shared.t.Log(args...) - l.buffer.mutex.Lock() - defer l.buffer.mutex.Unlock() + l.shared.buffer.mutex.Lock() + defer l.shared.buffer.mutex.Unlock() // Store as text. - l.buffer.text.WriteString(string(what)) + l.shared.buffer.text.WriteString(string(what)) for i := 1; i < len(args); i++ { - l.buffer.text.WriteByte(' ') - l.buffer.text.WriteString(args[i].(string)) + l.shared.buffer.text.WriteByte(' ') + l.shared.buffer.text.WriteString(args[i].(string)) } lastArg := args[len(args)-1].(string) if lastArg[len(lastArg)-1] != '\n' { - l.buffer.text.WriteByte('\n') + l.shared.buffer.text.WriteByte('\n') } // Store as raw data. - l.buffer.log = append(l.buffer.log, + l.shared.buffer.log = append(l.shared.buffer.log, LogEntry{ Timestamp: time.Now(), Type: what, @@ -310,27 +391,25 @@ func (l *tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, // 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 +func (l tlogger) 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 *tlogger) WithValues(kvList ...interface{}) logr.LogSink { - new := *l - new.values = serialize.WithValues(l.values, kvList) - return &new +func (l tlogger) WithValues(kvList ...interface{}) logr.LogSink { + l.values = serialize.WithValues(l.values, kvList) + return l } -func (l *tlogger) GetUnderlying() TL { - return l.t +func (l tlogger) GetUnderlying() TL { + return l.shared.t } -func (l *tlogger) GetBuffer() Buffer { - return l.buffer +func (l tlogger) GetBuffer() Buffer { + return &l.shared.buffer } var _ logr.LogSink = &tlogger{} diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index c4efa4629..bb1efe6a1 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -10,9 +10,14 @@ package ktesting_test import ( "bytes" "errors" + "flag" "fmt" + "regexp" + "runtime" + "sync" "testing" + "k8s.io/klog/v2" "k8s.io/klog/v2/ktesting" ) @@ -132,6 +137,7 @@ func TestCallDepth(t *testing.T) { } type logToBuf struct { + ktesting.NopTL bytes.Buffer } @@ -147,3 +153,99 @@ func (l *logToBuf) Log(args ...interface{}) { } l.Write([]byte("\n")) } + +func TestStop(t *testing.T) { + // This test is set up so that a subtest spawns a goroutine and that + // goroutine logs through ktesting *after* the subtest has + // completed. This is not supported by testing.T.Log and normally + // leads to: + // panic: Log in goroutine after TestGoroutines/Sub has completed: INFO hello world + // + // It works with ktesting if (and only if) logging gets redirected to klog + // before returning from the test. + + // Capture output for testing. + state := klog.CaptureState() + defer state.Restore() + var output bytes.Buffer + var fs flag.FlagSet + klog.InitFlags(&fs) + fs.Set("alsologtostderr", "false") + fs.Set("logtostderr", "false") + fs.Set("stderrthreshold", "FATAL") + fs.Set("one_output", "true") + klog.SetOutput(&output) + + var logger klog.Logger + var line int + var wg1, wg2 sync.WaitGroup + wg1.Add(1) + wg2.Add(1) + t.Run("Sub", func(t *testing.T) { + logger, _ = ktesting.NewTestContext(t) + go func() { + defer wg2.Done() + + // Wait for test to have returned. + wg1.Wait() + + // This output must go to klog because the test has + // completed. + _, _, line, _ = runtime.Caller(0) + logger.Info("simple info message") + logger.Error(nil, "error message") + logger.WithName("me").WithValues("completed", true).Info("complex info message", "anotherValue", 1) + }() + }) + // Allow goroutine above to proceed. + wg1.Done() + + // Ensure that goroutine has completed. + wg2.Wait() + + actual := output.String() + + // Strip time and pid prefix. + actual = regexp.MustCompile(`(?m)^.* testinglogger_test.go:`).ReplaceAllString(actual, `testinglogger_test.go:`) + + // All lines from the callstack get stripped. We can be sure that it was non-empty because otherwise we wouldn't + // have the < > markers. + // + // Full output: + // testinglogger_test.go:194] "TestStop/Sub leaked goroutine: WARNING: test kept at least one goroutine running after test completion" callstack=< + // goroutine 23 [running]: + // k8s.io/klog/v2/internal/dbg.Stacks(0x0) + // /nvme/gopath/src/k8s.io/klog/internal/dbg/dbg.go:34 +0x8a + // k8s.io/klog/v2/ktesting.tlogger.fallbackLogger({0xc0000f2780, {0x0, 0x0}, {0x0, 0x0, 0x0}}) + // /nvme/gopath/src/k8s.io/klog/ktesting/testinglogger.go:292 +0x232 + // k8s.io/klog/v2/ktesting.tlogger.Info({0xc0000f2780, {0x0, 0x0}, {0x0, 0x0, 0x0}}, 0x0, {0x5444a5, 0x13}, {0x0, ...}) + // /nvme/gopath/src/k8s.io/klog/ktesting/testinglogger.go:316 +0x28a + // github.com/go-logr/logr.Logger.Info({{0x572438?, 0xc0000c0ff0?}, 0x0?}, {0x5444a5, 0x13}, {0x0, 0x0, 0x0}) + // /nvme/gopath/pkg/mod/github.com/go-logr/logr@v1.2.0/logr.go:249 +0xd0 + // k8s.io/klog/v2/ktesting_test.TestStop.func1.1() + // /nvme/gopath/src/k8s.io/klog/ktesting/testinglogger_test.go:194 +0xe5 + // created by k8s.io/klog/v2/ktesting_test.TestStop.func1 + // /nvme/gopath/src/k8s.io/klog/ktesting/testinglogger_test.go:185 +0x105 + // > + actual = regexp.MustCompile(`(?m)^\t.*?\n`).ReplaceAllString(actual, ``) + + expected := fmt.Sprintf(`testinglogger_test.go:%d] "TestStop/Sub leaked goroutine: WARNING: test kept at least one goroutine running after test completion" callstack=< + > +testinglogger_test.go:%d] "TestStop/Sub leaked goroutine: simple info message" +testinglogger_test.go:%d] "TestStop/Sub leaked goroutine: error message" +testinglogger_test.go:%d] "TestStop/Sub leaked goroutine/me: complex info message" completed=true anotherValue=1 +`, + line+1, line+1, line+2, line+3) + if actual != expected { + t.Errorf("Output does not match. Expected:\n%s\nActual:\n%s\n", expected, actual) + } + + testingLogger, ok := logger.GetSink().(ktesting.Underlier) + if !ok { + t.Fatal("should have had a ktesting logger") + } + captured := testingLogger.GetBuffer().String() + if captured != "" { + t.Errorf("testing logger should not have captured any output, got instead:\n%s", captured) + } +} From c2d5a451f664994c87cfcf64e414d537a5892751 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 6 Jul 2022 14:33:03 +0200 Subject: [PATCH 096/125] contextual logging: enable by default again Commit 3c90bf9a79bf6ed7c82fa8cafeaf1681c8555255 accidentally changed the default for contextual logging from "enabled" to "disabled". The intention always was and still is to make the new API do something useful by default and only be more cautious in key Kubernetes binaries which have a feature gate. --- klog.go | 6 +++- ktesting/contextual_test.go | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ktesting/contextual_test.go diff --git a/klog.go b/klog.go index 8305e2520..00715b111 100644 --- a/klog.go +++ b/klog.go @@ -549,7 +549,11 @@ type loggingT struct { vmap map[uintptr]Level } -var logging loggingT +var logging = loggingT{ + settings: settings{ + contextualLoggingEnabled: true, + }, +} // setVState sets a consistent state for V logging. // l.mu is held. diff --git a/ktesting/contextual_test.go b/ktesting/contextual_test.go new file mode 100644 index 000000000..f260fb0aa --- /dev/null +++ b/ktesting/contextual_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2019 The Kubernetes Authors. +Copyright 2020 Intel Coporation. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ktesting_test + +import ( + "context" + "testing" + + "k8s.io/klog/v2" + "k8s.io/klog/v2/ktesting" +) + +func TestContextual(t *testing.T) { + logger, ctx := ktesting.NewTestContext(t) + + doSomething(ctx) + + // When contextual logging is disabled, the output goes to klog + // instead of the testing logger. + state := klog.CaptureState() + defer state.Restore() + klog.EnableContextualLogging(false) + doSomething(ctx) + + testingLogger, ok := logger.GetSink().(ktesting.Underlier) + if !ok { + t.Fatal("Should have had a ktesting LogSink!?") + } + + actual := testingLogger.GetBuffer().String() + expected := `INFO hello world +INFO foo: hello also from me +` + if actual != expected { + t.Errorf("mismatch in captured output, expected:\n%s\ngot:\n%s\n", expected, actual) + } +} + +func doSomething(ctx context.Context) { + logger := klog.FromContext(ctx) + logger.Info("hello world") + + logger = logger.WithName("foo") + ctx = klog.NewContext(ctx, logger) + doSomeMore(ctx) +} + +func doSomeMore(ctx context.Context) { + logger := klog.FromContext(ctx) + logger.Info("hello also from me") +} From 442891b6306390bc60ffb68b94f99fece207de46 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 7 Jul 2022 11:29:11 +0200 Subject: [PATCH 097/125] OWNERS: add harshanarayana Good track record of recent reviews and volunteered to help also in the future. --- OWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/OWNERS b/OWNERS index 8cccebf2e..a2fe8f351 100644 --- a/OWNERS +++ b/OWNERS @@ -1,5 +1,6 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: + - harshanarayana - pohly approvers: - dims From dcddc5f51aea4b875ac0026e7bb7671da8fb1fa0 Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Wed, 3 Aug 2022 18:18:45 +0530 Subject: [PATCH 098/125] kvlistformat: fix the issue with display marshalled value for non string type --- internal/serialize/keyvalues.go | 2 +- internal/serialize/keyvalues_test.go | 29 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index f85d7ccf8..ad6bf1116 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -145,7 +145,7 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { case string: writeStringValue(b, true, value) default: - writeStringValue(b, false, fmt.Sprintf("%+v", v)) + writeStringValue(b, false, fmt.Sprintf("%+v", value)) } case []byte: // In https://github.com/kubernetes/klog/pull/237 it was decided diff --git a/internal/serialize/keyvalues_test.go b/internal/serialize/keyvalues_test.go index d4b338519..1bf4842b7 100644 --- a/internal/serialize/keyvalues_test.go +++ b/internal/serialize/keyvalues_test.go @@ -39,6 +39,27 @@ func (p point) String() string { return fmt.Sprintf("x=%d, y=%d", p.x, p.y) } +type dummyStruct struct { + key string + value string +} + +func (d *dummyStruct) MarshalLog() interface{} { + return map[string]string{ + "key-data": d.key, + "value-data": d.value, + } +} + +type dummyStructWithStringMarshal struct { + key string + value string +} + +func (d *dummyStructWithStringMarshal) MarshalLog() interface{} { + return fmt.Sprintf("%s=%s", d.key, d.value) +} + // Test that kvListFormat works as advertised. func TestKvListFormat(t *testing.T) { var emptyPoint *point @@ -46,6 +67,14 @@ func TestKvListFormat(t *testing.T) { keysValues []interface{} want string }{ + { + keysValues: []interface{}{"data", &dummyStruct{key: "test", value: "info"}}, + want: " data=map[key-data:test value-data:info]", + }, + { + keysValues: []interface{}{"data", &dummyStructWithStringMarshal{key: "test", value: "info"}}, + want: ` data="test=info"`, + }, { keysValues: []interface{}{"pod", "kubedns"}, want: " pod=\"kubedns\"", From 6f5eeb9e4815c4e6db8eeeb700e74472703eed77 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 9 Aug 2022 14:30:05 -0400 Subject: [PATCH 099/125] Bump version of golang to 1.19 and prune older 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 11aecceb8..cca0121f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.15, 1.16, 1.17, 1.18] + go-version: [1.17, 1.18, 1.19] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From 8c0f205d4582113ac3e93a4836f48b9bdccad462 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 9 Aug 2022 14:30:40 -0400 Subject: [PATCH 100/125] reformat to golang 1.19 standards Signed-off-by: Davanum Srinivas --- contextual.go | 5 +-- klog.go | 76 +++++++++++++++++++++------------------ ktesting/init/init.go | 2 +- ktesting/options.go | 14 ++++---- ktesting/setup.go | 4 +-- ktesting/testinglogger.go | 32 ++++++++--------- test/output.go | 8 ++--- test/zapr.go | 24 ++++++------- textlogger/options.go | 14 ++++---- textlogger/textlogger.go | 4 +-- 10 files changed, 95 insertions(+), 88 deletions(-) diff --git a/contextual.go b/contextual.go index 65ac479ab..2428963c0 100644 --- a/contextual.go +++ b/contextual.go @@ -47,8 +47,9 @@ var ( // If set, all log lines will be suppressed from the regular output, and // redirected to the logr implementation. // Use as: -// ... -// klog.SetLogger(zapr.NewLogger(zapLog)) +// +// ... +// klog.SetLogger(zapr.NewLogger(zapLog)) // // To remove a backing logr implemention, use ClearLogger. Setting an // empty logger with SetLogger(logr.Logger{}) does not work. diff --git a/klog.go b/klog.go index 652fadcd4..628117e6d 100644 --- a/klog.go +++ b/klog.go @@ -39,39 +39,38 @@ // This package provides several flags that modify this behavior. // As a result, flag.Parse must be called before any logging is done. // -// -logtostderr=true -// Logs are written to standard error instead of to files. -// This shortcuts most of the usual output routing: -// -alsologtostderr, -stderrthreshold and -log_dir have no -// effect and output redirection at runtime with SetOutput is -// ignored. -// -alsologtostderr=false -// Logs are written to standard error as well as to files. -// -stderrthreshold=ERROR -// Log events at or above this severity are logged to standard -// error as well as to files. -// -log_dir="" -// Log files will be written to this directory instead of the -// default temporary directory. +// -logtostderr=true +// Logs are written to standard error instead of to files. +// This shortcuts most of the usual output routing: +// -alsologtostderr, -stderrthreshold and -log_dir have no +// effect and output redirection at runtime with SetOutput is +// ignored. +// -alsologtostderr=false +// Logs are written to standard error as well as to files. +// -stderrthreshold=ERROR +// Log events at or above this severity are logged to standard +// error as well as to files. +// -log_dir="" +// Log files will be written to this directory instead of the +// default temporary directory. // -// Other flags provide aids to debugging. -// -// -log_backtrace_at="" -// When set to a file and line number holding a logging statement, -// such as -// -log_backtrace_at=gopherflakes.go:234 -// a stack trace will be written to the Info log whenever execution -// hits that statement. (Unlike with -vmodule, the ".go" must be -// present.) -// -v=0 -// Enable V-leveled logging at the specified level. -// -vmodule="" -// The syntax of the argument is a comma-separated list of pattern=N, -// where pattern is a literal file name (minus the ".go" suffix) or -// "glob" pattern and N is a V level. For instance, -// -vmodule=gopher*=3 -// sets the V level to 3 in all Go files whose names begin "gopher". +// Other flags provide aids to debugging. // +// -log_backtrace_at="" +// When set to a file and line number holding a logging statement, +// such as +// -log_backtrace_at=gopherflakes.go:234 +// a stack trace will be written to the Info log whenever execution +// hits that statement. (Unlike with -vmodule, the ".go" must be +// present.) +// -v=0 +// Enable V-leveled logging at the specified level. +// -vmodule="" +// The syntax of the argument is a comma-separated list of pattern=N, +// where pattern is a literal file name (minus the ".go" suffix) or +// "glob" pattern and N is a V level. For instance, +// -vmodule=gopher*=3 +// sets the V level to 3 in all Go files whose names begin "gopher". package klog import ( @@ -633,8 +632,11 @@ It returns a buffer containing the formatted header and the user's file and line The depth specifies how many stack frames above lives the source line to be identified in the log message. Log lines have this form: + Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... + where the fields are defined as follows: + L A single character, representing the log level (eg 'I' for INFO) mm The month (zero padded; ie May is '05') dd The day (zero padded) @@ -1298,9 +1300,13 @@ 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 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 // not evaluate its arguments. // @@ -1582,10 +1588,10 @@ func ErrorSDepth(depth int, err error, msg string, keysAndValues ...interface{}) // // Callers who want more control over handling of fatal events may instead use a // combination of different functions: -// - some info or error logging function, optionally with a stack trace -// value generated by github.com/go-logr/lib/dbg.Backtrace -// - Flush to flush pending log data -// - panic, os.Exit or returning to the caller with an error +// - some info or error logging function, optionally with a stack trace +// value generated by github.com/go-logr/lib/dbg.Backtrace +// - Flush to flush pending log data +// - panic, os.Exit or returning to the caller with an error // // Arguments are handled in the manner of fmt.Print; a newline is appended if missing. func Fatal(args ...interface{}) { diff --git a/ktesting/init/init.go b/ktesting/init/init.go index c1fa145a8..7eb4dc675 100644 --- a/ktesting/init/init.go +++ b/ktesting/init/init.go @@ -18,7 +18,7 @@ limitations under the License. // the flag.CommandLine. This is done during initialization, so merely // importing it is enough. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/ktesting/options.go b/ktesting/options.go index 8cb13c406..743b51eba 100644 --- a/ktesting/options.go +++ b/ktesting/options.go @@ -29,7 +29,7 @@ import ( // // Must be constructed with NewConfig. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -40,7 +40,7 @@ type Config struct { // ConfigOption implements functional parameters for NewConfig. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -54,7 +54,7 @@ type configOptions struct { // VerbosityFlagName overrides the default -testing.v for the verbosity level. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -67,7 +67,7 @@ func VerbosityFlagName(name string) ConfigOption { // VModulFlagName overrides the default -testing.vmodule for the per-module // verbosity levels. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -84,7 +84,7 @@ func VModuleFlagName(name string) ConfigOption { // which is useful when debugging a failed test. `go test` only shows the log // output for failed tests. To see all output, use `go test -v`. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -97,7 +97,7 @@ func Verbosity(level int) ConfigOption { // NewConfig returns a configuration with recommended defaults and optional // modifications. Command line flags are not bound to any FlagSet yet. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -120,7 +120,7 @@ func NewConfig(opts ...ConfigOption) *Config { // AddFlags registers the command line flags that control the configuration. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/ktesting/setup.go b/ktesting/setup.go index d3029ae96..16b218ecd 100644 --- a/ktesting/setup.go +++ b/ktesting/setup.go @@ -25,7 +25,7 @@ import ( // DefaultConfig is the global default logging configuration for a unit // test. It is used by NewTestContext and k8s.io/klogr/testing/init. // -// Experimental +// # Experimental // // Notice: This variable is EXPERIMENTAL and may be changed or removed in a // later release. @@ -36,7 +36,7 @@ var DefaultConfig = NewConfig() // will receive all log output. Importing k8s.io/klogr/testing/init will add // command line flags that modify the configuration of that log output. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index b3272845d..67bbd857f 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -25,17 +25,17 @@ limitations under the License. // access to that data, cast the LogSink into the Underlier type and retrieve // it: // -// logger := ktesting.NewLogger(...) -// if testingLogger, ok := logger.GetSink().(ktesting.Underlier); ok { -// t := testingLogger.GetUnderlying() -// buffer := testingLogger.GetBuffer() -// text := buffer.String() -// log := buffer.Data() +// logger := ktesting.NewLogger(...) +// if testingLogger, ok := logger.GetSink().(ktesting.Underlier); ok { +// t := testingLogger.GetUnderlying() +// buffer := testingLogger.GetBuffer() +// text := buffer.String() +// log := buffer.Data() // // Serialization of the structured log parameters is done in the same way // as for klog.InfoS. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. @@ -57,7 +57,7 @@ import ( // TL is the relevant subset of testing.TB. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -69,7 +69,7 @@ type TL interface { // NopTL implements TL with empty stubs. It can be used when only capturing // output in memory is relevant. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -88,7 +88,7 @@ var _ TL = NopTL{} // that output will be printed via the global klog logger with // ` leaked goroutine` as prefix. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -117,7 +117,7 @@ func NewLogger(t TL, c *Config) logr.Logger { // Buffer stores log entries as formatted text and structured data. // It is safe to use this concurrently. // -// Experimental +// # Experimental // // Notice: This interface is EXPERIMENTAL and may be changed or removed in a // later release. @@ -132,7 +132,7 @@ type Buffer interface { // Log contains log entries in the order in which they were generated. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -141,7 +141,7 @@ type Log []LogEntry // DeepCopy returns a copy of the log. The error instance and key/value // pairs remain shared. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -153,7 +153,7 @@ func (l Log) DeepCopy() Log { // LogEntry represents all information captured for a log entry. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -190,7 +190,7 @@ type LogEntry struct { // LogType determines whether a log entry was created with an Error or Info // call. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -217,7 +217,7 @@ const ( // Underlier is implemented by the LogSink of this logger. It provides access // to additional APIs that are normally hidden behind the Logger API. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/test/output.go b/test/output.go index 775571f2f..5b80ebcdf 100644 --- a/test/output.go +++ b/test/output.go @@ -16,7 +16,7 @@ limitations under the License. // Package test contains a reusable unit test for logging output and behavior. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. @@ -44,7 +44,7 @@ import ( // InitKlog must be called once in an init function of a test package to // configure klog for testing with Output. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -61,7 +61,7 @@ func InitKlog() { // OutputConfig contains optional settings for Output. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -103,7 +103,7 @@ type OutputConfig struct { // Loggers will be tested with direct calls to Info or // as backend for klog. // -// Experimental +// # 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 diff --git a/test/zapr.go b/test/zapr.go index 427d1f78a..9f6cd8b4c 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -19,7 +19,7 @@ package test // ZaprOutputMappingDirect provides a mapping from klog output to the // corresponding zapr output when zapr is called directly. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. @@ -241,18 +241,18 @@ I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" // klog. // // This is different from ZaprOutputMappingDirect because: -// - WithName gets added to the message by Output. -// - zap uses . as separator instead of / between WithName values, -// here we get slashes because Output concatenates these values. -// - WithValues are added to the normal key/value parameters by -// Output, which puts them after "v". -// - Output does that without emitting the warning that we get -// from zapr. -// - zap drops keys with missing values, here we get "(MISSING)". -// - zap does not de-duplicate key/value pairs, here klog does that -// for it. +// - WithName gets added to the message by Output. +// - zap uses . as separator instead of / between WithName values, +// here we get slashes because Output concatenates these values. +// - WithValues are added to the normal key/value parameters by +// Output, which puts them after "v". +// - Output does that without emitting the warning that we get +// from zapr. +// - zap drops keys with missing values, here we get "(MISSING)". +// - zap does not de-duplicate key/value pairs, here klog does that +// for it. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/textlogger/options.go b/textlogger/options.go index 28188ddf3..5803ccecb 100644 --- a/textlogger/options.go +++ b/textlogger/options.go @@ -31,7 +31,7 @@ import ( // // Must be constructed with NewConfig. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -42,7 +42,7 @@ type Config struct { // ConfigOption implements functional parameters for NewConfig. // -// Experimental +// # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. @@ -66,7 +66,7 @@ func VerbosityFlagName(name string) ConfigOption { // VModulFlagName overrides the default -vmodule for the per-module // verbosity levels. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -80,7 +80,7 @@ func VModuleFlagName(name string) ConfigOption { // See https://github.com/kubernetes/community/blob/9406b4352fe2d5810cb21cc3cb059ce5886de157/contributors/devel/sig-instrumentation/logging.md#logging-conventions // for log level conventions in Kubernetes. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -92,7 +92,7 @@ func Verbosity(level int) ConfigOption { // Output overrides stderr as the output stream. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -105,7 +105,7 @@ func Output(output io.Writer) ConfigOption { // NewConfig returns a configuration with recommended defaults and optional // modifications. Command line flags are not bound to any FlagSet yet. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. @@ -129,7 +129,7 @@ func NewConfig(opts ...ConfigOption) *Config { // AddFlags registers the command line flags that control the configuration. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 17c7584f6..17706d6da 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -18,7 +18,7 @@ limitations under the License. // Package textlogger contains an implementation of the logr interface // which is producing the exact same output as klog. // -// Experimental +// # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release. @@ -50,7 +50,7 @@ var ( // NewLogger constructs a new logger. // -// Experimental +// # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. The behavior of the returned Logger may change. From a952486542153bb13e70d4df7e34707f801e0180 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 7 Sep 2022 14:46:14 +0200 Subject: [PATCH 101/125] add test for the command line usage The flags and their defaults are part of the klog API and must not change. --- klog_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/klog_test.go b/klog_test.go index 71717c0d0..1b5721042 100644 --- a/klog_test.go +++ b/klog_test.go @@ -764,6 +764,9 @@ func TestFileSizeCheck(t *testing.T) { } func TestInitFlags(t *testing.T) { + defer CaptureState().Restore() + setFlags() + fs1 := flag.NewFlagSet("test1", flag.PanicOnError) InitFlags(fs1) fs1.Set("log_dir", "/test1") @@ -779,6 +782,48 @@ func TestInitFlags(t *testing.T) { } } +func TestCommandLine(t *testing.T) { + var fs flag.FlagSet + InitFlags(&fs) + + expectedFlags := ` -add_dir_header + If true, adds the file directory to the header of the log messages + -alsologtostderr + log to standard error as well as files (no effect when -logtostderr=true) + -log_backtrace_at value + when logging hits line file:N, emit a stack trace + -log_dir string + If non-empty, write log files in this directory (no effect when -logtostderr=true) + -log_file string + If non-empty, use this log file (no effect when -logtostderr=true) + -log_file_max_size uint + Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + -logtostderr + log to standard error instead of files (default true) + -one_output + If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true) + -skip_headers + If true, avoid header prefixes in the log messages + -skip_log_headers + If true, avoid headers when opening log files (no effect when -logtostderr=true) + -stderrthreshold value + logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false) (default 2) + -v value + number for the log level verbosity + -vmodule value + comma-separated list of pattern=N settings for file-filtered logging +` + + var output bytes.Buffer + fs.SetOutput(&output) + fs.PrintDefaults() + actualFlags := output.String() + + if expectedFlags != actualFlags { + t.Fatalf("Command line changed.\nExpected:\n%q\nActual:\n%q\n", expectedFlags, actualFlags) + } +} + func TestInfoObjectRef(t *testing.T) { defer CaptureState().Restore() setFlags() From 28f790698e907ffbd2ac80fe4d6954429f827fbf Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 7 Sep 2022 14:50:34 +0200 Subject: [PATCH 102/125] make InitFlags read-only The way it was implemented before, the current value of several flags was written to the global config struct each time InitFlags was called. Not all flags may get written concurrently, so this led to a data race when some other goroutine was actively using klog. InitFlags also created new flag.Flag instances for each call. By creating flags once during init, InitFlags becomes entirely read-only and thus safe to use concurrently. What remains ambiguous is which flags may get changed at runtime. That was underspecified before and doesn't get fixed here either, it just gets called out as a potential problem. --- klog.go | 61 +++++++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/klog.go b/klog.go index 628117e6d..1bd11b675 100644 --- a/klog.go +++ b/klog.go @@ -396,45 +396,48 @@ type flushSyncWriter interface { io.Writer } -// init sets up the defaults. +var logging loggingT +var commandLine flag.FlagSet + +// init sets up the defaults and creates command line flags. func init() { + commandLine.StringVar(&logging.logDir, "log_dir", "", "If non-empty, write log files in this directory (no effect when -logtostderr=true)") + commandLine.StringVar(&logging.logFile, "log_file", "", "If non-empty, use this log file (no effect when -logtostderr=true)") + commandLine.Uint64Var(&logging.logFileMaxSizeMB, "log_file_max_size", 1800, + "Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. "+ + "If the value is 0, the maximum file size is unlimited.") + commandLine.BoolVar(&logging.toStderr, "logtostderr", true, "log to standard error instead of files") + commandLine.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files (no effect when -logtostderr=true)") + logging.setVState(0, nil, false) + commandLine.Var(&logging.verbosity, "v", "number for the log level verbosity") + commandLine.BoolVar(&logging.addDirHeader, "add_dir_header", false, "If true, adds the file directory to the header of the log messages") + commandLine.BoolVar(&logging.skipHeaders, "skip_headers", false, "If true, avoid header prefixes in the log messages") + commandLine.BoolVar(&logging.oneOutput, "one_output", false, "If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true)") + commandLine.BoolVar(&logging.skipLogHeaders, "skip_log_headers", false, "If true, avoid headers when opening log files (no effect when -logtostderr=true)") logging.stderrThreshold = severityValue{ Severity: severity.ErrorLog, // Default stderrThreshold is ERROR. } - logging.setVState(0, nil, false) - logging.logDir = "" - logging.logFile = "" - logging.logFileMaxSizeMB = 1800 - logging.toStderr = true - logging.alsoToStderr = false - logging.skipHeaders = false - logging.addDirHeader = false - logging.skipLogHeaders = false - logging.oneOutput = false + commandLine.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false)") + commandLine.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") + commandLine.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") + + logging.settings.contextualLoggingEnabled = true logging.flushD = newFlushDaemon(logging.lockAndFlushAll, nil) } // InitFlags is for explicitly initializing the flags. +// It may get called repeatedly for different flagsets, but not +// twice for the same one. May get called concurrently +// to other goroutines using klog. However, only some flags +// may get set concurrently (see implementation). func InitFlags(flagset *flag.FlagSet) { if flagset == nil { flagset = flag.CommandLine } - flagset.StringVar(&logging.logDir, "log_dir", logging.logDir, "If non-empty, write log files in this directory (no effect when -logtostderr=true)") - flagset.StringVar(&logging.logFile, "log_file", logging.logFile, "If non-empty, use this log file (no effect when -logtostderr=true)") - flagset.Uint64Var(&logging.logFileMaxSizeMB, "log_file_max_size", logging.logFileMaxSizeMB, - "Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. "+ - "If the value is 0, the maximum file size is unlimited.") - flagset.BoolVar(&logging.toStderr, "logtostderr", logging.toStderr, "log to standard error instead of files") - flagset.BoolVar(&logging.alsoToStderr, "alsologtostderr", logging.alsoToStderr, "log to standard error as well as files (no effect when -logtostderr=true)") - flagset.Var(&logging.verbosity, "v", "number for the log level verbosity") - flagset.BoolVar(&logging.addDirHeader, "add_dir_header", logging.addDirHeader, "If true, adds the file directory to the header of the log messages") - flagset.BoolVar(&logging.skipHeaders, "skip_headers", logging.skipHeaders, "If true, avoid header prefixes in the log messages") - flagset.BoolVar(&logging.oneOutput, "one_output", logging.oneOutput, "If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true)") - flagset.BoolVar(&logging.skipLogHeaders, "skip_log_headers", logging.skipLogHeaders, "If true, avoid headers when opening log files (no effect when -logtostderr=true)") - flagset.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false)") - flagset.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") - flagset.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") + commandLine.VisitAll(func(f *flag.Flag) { + flagset.Var(f.Value, f.Name, f.Usage) + }) } // Flush flushes all pending log I/O. @@ -549,12 +552,6 @@ type loggingT struct { vmap map[uintptr]Level } -var logging = loggingT{ - settings: settings{ - contextualLoggingEnabled: true, - }, -} - // setVState sets a consistent state for V logging. // l.mu is held. func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool) { From f08fd2612f5affc0d80c30a49527fc6f1e2bd983 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 27 Oct 2022 18:29:19 +0200 Subject: [PATCH 103/125] klog: benchmark the overhead when logging is off Most of the log calls in a program do not emit log data, but the parameters for the call need to be prepared anyway, which causes overhead. --- klog_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 5 deletions(-) diff --git a/klog_test.go b/klog_test.go index 1b5721042..e6e8922cc 100644 --- a/klog_test.go +++ b/klog_test.go @@ -669,6 +669,10 @@ func BenchmarkKObj(b *testing.B) { // emits a log entry because verbosity is lower than 5. // For performance when the result of KObjs gets formatted, // see examples/benchmarks. +// +// This uses two different patterns: +// - directly calling klog.V(5).Info +// - guarding the call with Enabled func BenchmarkKObjs(b *testing.B) { for length := 0; length <= 100; length += 10 { b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { @@ -676,17 +680,119 @@ func BenchmarkKObjs(b *testing.B) { for i := 0; i < length; i++ { arg[i] = test.KMetadataMock{Name: "a", NS: "a"} } - b.ResetTimer() - var r interface{} - for i := 0; i < b.N; i++ { - r = KObjs(arg) + b.Run("simple", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + V(5).InfoS("benchmark", "objs", KObjs(arg)) + } + }) + + b.Run("conditional", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if klogV := V(5); klogV.Enabled() { + klogV.InfoS("benchmark", "objs", KObjs(arg)) + } + } + }) + }) + } +} + +// BenchmarkKObjSlice corresponds to BenchmarkKObjs except that it uses KObjSlice +func BenchmarkKObjSlice(b *testing.B) { + for length := 0; length <= 100; length += 10 { + b.Run(fmt.Sprintf("%d", length), func(b *testing.B) { + arg := make([]interface{}, length) + for i := 0; i < length; i++ { + arg[i] = test.KMetadataMock{Name: "a", NS: "a"} } - result = r + + b.Run("simple", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + V(5).InfoS("benchmark", "objs", KObjSlice(arg)) + } + }) + + b.Run("conditional", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if klogV := V(5); klogV.Enabled() { + klogV.InfoS("benchmark", "objs", KObjSlice(arg)) + } + } + }) }) } } +// BenchmarkScalars corresponds to BenchmarkKObjs except that it avoids function +// calls for the parameters. +func BenchmarkScalars(b *testing.B) { + b.Run("simple", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + V(5).InfoS("benchmark", "str", "hello world", "int", 42) + } + }) + + b.Run("conditional", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if klogV := V(5); klogV.Enabled() { + klogV.InfoS("benchmark", "str", "hello world", "int", 42) + } + } + }) +} + +// BenchmarkScalarsWithLogger is the same as BenchmarkScalars except that it uses +// a go-logr instance. +func BenchmarkScalarsWithLogger(b *testing.B) { + logger := Background() + b.Run("simple", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.V(5).Info("benchmark", "str", "hello world", "int", 42) + } + }) + + b.Run("conditional", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if loggerV := logger.V(5); loggerV.Enabled() { + loggerV.Info("benchmark", "str", "hello world", "int", 42) + } + } + }) +} + +// BenchmarKObjSliceWithLogger is the same as BenchmarkKObjSlice except that it +// uses a go-logr instance and a slice with a single entry. BenchmarkKObjSlice +// shows that the overhead for KObjSlice is constant and doesn't depend on the +// slice length when logging is off. +func BenchmarkKObjSliceWithLogger(b *testing.B) { + logger := Background() + arg := []interface{}{test.KMetadataMock{Name: "a", NS: "a"}} + b.Run("simple", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.V(5).Info("benchmark", "objs", KObjSlice(arg)) + } + }) + + b.Run("conditional", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if loggerV := logger.V(5); loggerV.Enabled() { + loggerV.Info("benchmark", "objs", KObjSlice(arg)) + } + } + }) +} + func BenchmarkLogs(b *testing.B) { defer CaptureState().Restore() setFlags() From d20ee72ae54108f6e0f463b13d6c4aa9d7a3f0f5 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 16 Jan 2023 10:58:02 +0100 Subject: [PATCH 104/125] klogr: avoid calling Info/Error/Enabled through auto-generated code These functions get called for a pointer because that is what we use to implement the LogSink interface. The variant of these functions which take a value instead of a pointer was never used, so providing directly what is needed instead of relying on the compiler to generate that is more efficient (less code, avoids one extra callstack entry at runtime). The With* functions must continue to take a value because they need to modify that copy. --- klogr.go | 6 +++--- klogr/klogr.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/klogr.go b/klogr.go index 027a4014a..f0d3566ad 100644 --- a/klogr.go +++ b/klogr.go @@ -42,7 +42,7 @@ func (l *klogger) Init(info logr.RuntimeInfo) { l.callDepth += info.CallDepth } -func (l klogger) Info(level int, msg string, kvList ...interface{}) { +func (l *klogger) Info(level int, msg string, kvList ...interface{}) { merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg @@ -50,11 +50,11 @@ func (l klogger) Info(level int, msg string, kvList ...interface{}) { V(Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) } -func (l klogger) Enabled(level int) bool { +func (l *klogger) Enabled(level int) bool { return V(Level(level)).Enabled() } -func (l klogger) Error(err error, msg string, kvList ...interface{}) { +func (l *klogger) Error(err error, msg string, kvList ...interface{}) { merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg diff --git a/klogr/klogr.go b/klogr/klogr.go index c94e551ed..c56a46194 100644 --- a/klogr/klogr.go +++ b/klogr/klogr.go @@ -119,7 +119,7 @@ func pretty(value interface{}) string { return strings.TrimSpace(string(buffer.Bytes())) } -func (l klogger) Info(level int, msg string, kvList ...interface{}) { +func (l *klogger) Info(level int, msg string, kvList ...interface{}) { switch l.format { case FormatSerialize: msgStr := flatten("msg", msg) @@ -135,11 +135,11 @@ func (l klogger) Info(level int, msg string, kvList ...interface{}) { } } -func (l klogger) Enabled(level int) bool { +func (l *klogger) Enabled(level int) bool { return klog.V(klog.Level(level)).Enabled() } -func (l klogger) Error(err error, msg string, kvList ...interface{}) { +func (l *klogger) Error(err error, msg string, kvList ...interface{}) { msgStr := flatten("msg", msg) var loggableErr interface{} if err != nil { From c34db96b287543f92b5b8ce6fa09f84b189712bb Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 10:01:48 +0100 Subject: [PATCH 105/125] test: move InitKlog into tests This makes it possible to use test.Output inside packages that have other tests. --- examples/output_test/output_test.go | 12 ++++++---- test/output.go | 34 +++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index ac10f92f8..bd4e4787f 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -34,17 +34,15 @@ import ( "k8s.io/klog/v2/textlogger" ) -func init() { - test.InitKlog() -} - // TestKlogOutput tests klog output without a logger. func TestKlogOutput(t *testing.T) { + test.InitKlog(t) test.Output(t, test.OutputConfig{}) } // TestTextloggerOutput tests the textlogger, directly and as backend. func TestTextloggerOutput(t *testing.T) { + test.InitKlog(t) newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { config := textlogger.NewConfig( textlogger.Verbosity(v), @@ -65,6 +63,7 @@ func TestTextloggerOutput(t *testing.T) { // TestZaprOutput tests the zapr, directly and as backend. func TestZaprOutput(t *testing.T) { + test.InitKlog(t) newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { return newZaprLogger(out, v) } @@ -78,6 +77,7 @@ func TestZaprOutput(t *testing.T) { // TestKlogrOutput tests klogr output via klog. func TestKlogrOutput(t *testing.T) { + test.InitKlog(t) test.Output(t, test.OutputConfig{ NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger { return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog)) @@ -87,6 +87,7 @@ func TestKlogrOutput(t *testing.T) { // TestKlogrStackText tests klogr.klogr -> klog -> text logger. func TestKlogrStackText(t *testing.T) { + test.InitKlog(t) newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { // Backend: text output. config := textlogger.NewConfig( @@ -110,6 +111,7 @@ func TestKlogrStackText(t *testing.T) { // (https://github.com/kubernetes/klog/issues/294) because klogr logging // records that. func TestKlogrStackZapr(t *testing.T) { + test.InitKlog(t) mapping := test.ZaprOutputMappingIndirect() // klogr doesn't warn about invalid KVs and just inserts @@ -150,6 +152,7 @@ func TestKlogrStackZapr(t *testing.T) { // TestKlogrInternalStackText tests klog.klogr (the simplified version used for contextual logging) -> klog -> text logger. func TestKlogrInternalStackText(t *testing.T) { + test.InitKlog(t) newLogger := func(out io.Writer, v int, vmodule string) logr.Logger { // Backend: text output. config := textlogger.NewConfig( @@ -173,6 +176,7 @@ func TestKlogrInternalStackText(t *testing.T) { // (https://github.com/kubernetes/klog/issues/294) because klogr logging // records that. func TestKlogrInternalStackZapr(t *testing.T) { + test.InitKlog(t) mapping := test.ZaprOutputMappingIndirect() // klogr doesn't warn about invalid KVs and just inserts diff --git a/test/output.go b/test/output.go index 5b80ebcdf..1b79a945e 100644 --- a/test/output.go +++ b/test/output.go @@ -41,22 +41,38 @@ import ( "k8s.io/klog/v2" ) -// InitKlog must be called once in an init function of a test package to -// configure klog for testing with Output. +// InitKlog must be called in a test to configure klog for testing. +// The previous klog configuration will be restored automatically +// after the test. +// +// The returned flag set has the klog flags registered. It can +// be used to make further changes to the klog configuration. // // # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a // later release. -func InitKlog() { +func InitKlog(tb testing.TB) *flag.FlagSet { + state := klog.CaptureState() + tb.Cleanup(state.Restore) + + expectNoError := func(err error) { + if err != nil { + tb.Fatalf("unexpected error: %v", err) + } + } + // 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") + var fs flag.FlagSet + klog.InitFlags(&fs) + expectNoError(fs.Set("v", "10")) + expectNoError(fs.Set("log_file", "/dev/null")) + expectNoError(fs.Set("logtostderr", "false")) + expectNoError(fs.Set("alsologtostderr", "false")) + expectNoError(fs.Set("stderrthreshold", "10")) + + return &fs } // OutputConfig contains optional settings for Output. From 125ecfe90fc00d831068cc960a4fccf0538fb591 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 16 Jan 2023 11:01:40 +0100 Subject: [PATCH 106/125] klogr: fix vmodule support When klogr called V(), it didn't pass the callstack offset, which broke evaluation of the -vmodule patterns. A new VWithOffset klog API is needed to fix this. A unit test for this will follow. --- klog.go | 9 ++++++++- klogr.go | 6 ++++-- klogr/klogr.go | 7 ++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/klog.go b/klog.go index 1bd11b675..bcf60cf97 100644 --- a/klog.go +++ b/klog.go @@ -1313,6 +1313,13 @@ func newVerbose(level Level, b bool) Verbose { // less than or equal to the value of the -vmodule pattern matching the source file // containing the call. func V(level Level) Verbose { + return VDepth(1, level) +} + +// VDepth is a variant of V that accepts a number of stack frames that will be +// skipped when checking the -vmodule patterns. VDepth(0) is equivalent to +// V(). +func VDepth(depth int, level Level) Verbose { // This function tries hard to be cheap unless there's work to do. // The fast path is two atomic loads and compares. @@ -1329,7 +1336,7 @@ func V(level Level) Verbose { // but if V logging is enabled we're slow anyway. logging.mu.Lock() defer logging.mu.Unlock() - if runtime.Callers(2, logging.pcs[:]) == 0 { + if runtime.Callers(2+depth, logging.pcs[:]) == 0 { return newVerbose(level, false) } // runtime.Callers returns "return PCs", but we want diff --git a/klogr.go b/klogr.go index f0d3566ad..15de00e21 100644 --- a/klogr.go +++ b/klogr.go @@ -47,11 +47,13 @@ func (l *klogger) Info(level int, msg string, kvList ...interface{}) { if l.prefix != "" { msg = l.prefix + ": " + msg } - V(Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) + // Skip this function. + VDepth(l.callDepth+1, Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) } func (l *klogger) Enabled(level int) bool { - return V(Level(level)).Enabled() + // Skip this function and logr.Logger.Info where Enabled is called. + return VDepth(l.callDepth+2, Level(level)).Enabled() } func (l *klogger) Error(err error, msg string, kvList ...interface{}) { diff --git a/klogr/klogr.go b/klogr/klogr.go index c56a46194..db5b5c789 100644 --- a/klogr/klogr.go +++ b/klogr/klogr.go @@ -125,18 +125,19 @@ func (l *klogger) Info(level int, msg string, kvList ...interface{}) { msgStr := flatten("msg", msg) merged := serialize.MergeKVs(l.values, kvList) kvStr := flatten(merged...) - klog.V(klog.Level(level)).InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", kvStr) + klog.VDepth(l.callDepth+1, klog.Level(level)).InfoDepth(l.callDepth+1, l.prefix, " ", msgStr, " ", kvStr) case FormatKlog: merged := serialize.MergeKVs(l.values, kvList) if l.prefix != "" { msg = l.prefix + ": " + msg } - klog.V(klog.Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) + klog.VDepth(l.callDepth+1, klog.Level(level)).InfoSDepth(l.callDepth+1, msg, merged...) } } func (l *klogger) Enabled(level int) bool { - return klog.V(klog.Level(level)).Enabled() + // Skip this function and logr.Logger.Info where Enabled is called. + return klog.VDepth(l.callDepth+2, klog.Level(level)).Enabled() } func (l *klogger) Error(err error, msg string, kvList ...interface{}) { From 34a9807648b5a355d0589317bdf18d5a62036c9d Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 10:18:20 +0100 Subject: [PATCH 107/125] klogr: use test.InitKlog Besides sharing code, it's also better in two regards: - restores state after test - avoids triple printing of errors --- klogr/klogr_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go index 22ec1dc30..53b9e450d 100644 --- a/klogr/klogr_test.go +++ b/klogr/klogr_test.go @@ -4,11 +4,11 @@ import ( "bytes" "encoding/json" "errors" - "flag" "strings" "testing" "k8s.io/klog/v2" + "k8s.io/klog/v2/test" "github.com/go-logr/logr" ) @@ -169,14 +169,9 @@ func testOutput(t *testing.T, format string) { klogr: new().V(0), text: "test", err: errors.New("whoops"), - // The message is printed to three different log files (info, warning, error), so we see it three times in our output buffer. expectedOutput: ` "msg"="test" "error"="whoops" - "msg"="test" "error"="whoops" - "msg"="test" "error"="whoops" `, expectedKlogOutput: `"test" err="whoops" -"test" err="whoops" -"test" err="whoops" `, }, } @@ -209,13 +204,8 @@ func testOutput(t *testing.T, format string) { } func TestOutput(t *testing.T) { - klog.InitFlags(nil) - flag.CommandLine.Set("v", "10") - flag.CommandLine.Set("skip_headers", "true") - flag.CommandLine.Set("logtostderr", "false") - flag.CommandLine.Set("alsologtostderr", "false") - flag.CommandLine.Set("stderrthreshold", "10") - flag.Parse() + fs := test.InitKlog(t) + fs.Set("skip_headers", "true") formats := []string{ formatNew, From 70aa79562e966db0c6333e69bb91e759bb7bed27 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 16 Jan 2023 11:01:40 +0100 Subject: [PATCH 108/125] textlogger: fix vmodule support When Logger.Info called Enabled, the wrong number of stack frames were skipped. A unit test for this will follow. --- textlogger/textlogger.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 17706d6da..3c6cda645 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -88,7 +88,9 @@ func (l *tlogger) WithCallDepth(depth int) logr.LogSink { } func (l *tlogger) Enabled(level int) bool { - return l.config.Enabled(verbosity.Level(level), 1) + // Skip this function and the Logger.Info call, then + // also any additional stack frames from WithCallDepth. + return l.config.Enabled(verbosity.Level(level), 2+l.callDepth) } func (l *tlogger) Info(level int, msg string, kvList ...interface{}) { From f833abb683b1ea07d877cb888344508cb41ebb63 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 10:15:11 +0100 Subject: [PATCH 109/125] move output tests into packages Previously it was necessary to enter the "examples" module to run output tests for code in the main module. Now "go test ./..." at the root or in individual directories also runs these tests. --- examples/output_test/output_test.go | 37 ---------------------- klogr/output_test.go | 37 ++++++++++++++++++++++ output_test.go | 43 ++++++++++++++++++++++++++ textlogger/output_test.go | 48 +++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 klogr/output_test.go create mode 100644 output_test.go create mode 100644 textlogger/output_test.go diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index bd4e4787f..697dc459d 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -34,33 +34,6 @@ import ( "k8s.io/klog/v2/textlogger" ) -// TestKlogOutput tests klog output without a logger. -func TestKlogOutput(t *testing.T) { - test.InitKlog(t) - test.Output(t, test.OutputConfig{}) -} - -// TestTextloggerOutput tests the textlogger, directly and as backend. -func TestTextloggerOutput(t *testing.T) { - test.InitKlog(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}) - }) -} - // TestZaprOutput tests the zapr, directly and as backend. func TestZaprOutput(t *testing.T) { test.InitKlog(t) @@ -75,16 +48,6 @@ func TestZaprOutput(t *testing.T) { }) } -// TestKlogrOutput tests klogr output via klog. -func TestKlogrOutput(t *testing.T) { - test.InitKlog(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.klogr -> klog -> text logger. func TestKlogrStackText(t *testing.T) { test.InitKlog(t) diff --git a/klogr/output_test.go b/klogr/output_test.go new file mode 100644 index 000000000..aa87e9530 --- /dev/null +++ b/klogr/output_test.go @@ -0,0 +1,37 @@ +/* +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 klogr_test + +import ( + "io" + "testing" + + "github.com/go-logr/logr" + + "k8s.io/klog/v2/klogr" + "k8s.io/klog/v2/test" +) + +// TestKlogrOutput tests klogr output via klog. +func TestKlogrOutput(t *testing.T) { + test.InitKlog(t) + test.Output(t, test.OutputConfig{ + NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger { + return klogr.NewWithOptions(klogr.WithFormat(klogr.FormatKlog)) + }, + }) +} diff --git a/output_test.go b/output_test.go new file mode 100644 index 000000000..7e38e12c3 --- /dev/null +++ b/output_test.go @@ -0,0 +1,43 @@ +/* +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 klog_test + +import ( + "io" + "testing" + + "github.com/go-logr/logr" + + "k8s.io/klog/v2" + "k8s.io/klog/v2/test" +) + +// TestKlogOutput tests klog output without a logger. +func TestKlogOutput(t *testing.T) { + test.InitKlog(t) + test.Output(t, test.OutputConfig{}) +} + +// TestKlogKlogrOutput tests klogr output via klog, using the klog/v2 klogr. +func TestKlogrOutput(t *testing.T) { + test.InitKlog(t) + test.Output(t, test.OutputConfig{ + NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger { + return klog.NewKlogr() + }, + }) +} diff --git a/textlogger/output_test.go b/textlogger/output_test.go new file mode 100644 index 000000000..9a3bc95e3 --- /dev/null +++ b/textlogger/output_test.go @@ -0,0 +1,48 @@ +/* +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 textlogger_test + +import ( + "io" + "testing" + + "github.com/go-logr/logr" + + "k8s.io/klog/v2/test" + "k8s.io/klog/v2/textlogger" +) + +// TestTextloggerOutput tests the textlogger, directly and as backend. +func TestTextloggerOutput(t *testing.T) { + test.InitKlog(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}) + }) +} From 757e6bbe51a81bf76db53bac6983301e2e557891 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 10:43:20 +0100 Subject: [PATCH 110/125] use output test cases also for benchmarking So far, the output tests were only used for correctness checking. But they cover a variety of scenarios for which there were no benchmarks, therefore it makes sense to also use them for that. --- examples/output_test/output_test.go | 13 +- output_test.go | 25 +- test/output.go | 804 +++++++++++++++------------- textlogger/output_test.go | 37 +- 4 files changed, 489 insertions(+), 390 deletions(-) diff --git a/examples/output_test/output_test.go b/examples/output_test/output_test.go index 697dc459d..872612f72 100644 --- a/examples/output_test/output_test.go +++ b/examples/output_test/output_test.go @@ -34,12 +34,13 @@ import ( "k8s.io/klog/v2/textlogger" ) +func newLogger(out io.Writer, v int, vmodule string) logr.Logger { + return newZaprLogger(out, v) +} + // TestZaprOutput tests the zapr, directly and as backend. func TestZaprOutput(t *testing.T) { test.InitKlog(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()}) }) @@ -48,6 +49,12 @@ func TestZaprOutput(t *testing.T) { }) } +// Benchmark direct zapr output. +func BenchmarkZaprOutput(b *testing.B) { + test.InitKlog(b) + test.Benchmark(b, test.OutputConfig{NewLogger: newLogger, ExpectedOutputMapping: test.ZaprOutputMappingDirect()}) +} + // TestKlogrStackText tests klogr.klogr -> klog -> text logger. func TestKlogrStackText(t *testing.T) { test.InitKlog(t) diff --git a/output_test.go b/output_test.go index 7e38e12c3..c5434d600 100644 --- a/output_test.go +++ b/output_test.go @@ -26,18 +26,27 @@ import ( "k8s.io/klog/v2/test" ) -// TestKlogOutput tests klog output without a logger. +// klogConfig tests klog output without a logger. +var klogConfig = test.OutputConfig{} + func TestKlogOutput(t *testing.T) { test.InitKlog(t) - test.Output(t, test.OutputConfig{}) + test.Output(t, klogConfig) +} + +func BenchmarkKlogOutput(b *testing.B) { + test.InitKlog(b) + test.Benchmark(b, klogConfig) +} + +// klogKlogrConfig tests klogr output via klog, using the klog/v2 klogr. +var klogKLogrConfig = test.OutputConfig{ + NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger { + return klog.NewKlogr() + }, } -// TestKlogKlogrOutput tests klogr output via klog, using the klog/v2 klogr. func TestKlogrOutput(t *testing.T) { test.InitKlog(t) - test.Output(t, test.OutputConfig{ - NewLogger: func(out io.Writer, v int, vmodule string) logr.Logger { - return klog.NewKlogr() - }, - }) + test.Output(t, klogKLogrConfig) } diff --git a/test/output.go b/test/output.go index 1b79a945e..16553f2b4 100644 --- a/test/output.go +++ b/test/output.go @@ -108,6 +108,389 @@ type OutputConfig struct { SupportsVModule bool } +type testcase 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 +} + +var tests = map[string]testcase{ + "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: "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": { + 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" +`, + }, + "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: "odd arguments", + values: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `I output.go:] "odd arguments" 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: "both odd", + values: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `I output.go:] "both odd" 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] +`, + }, + "KObjSlice okay": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice([]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]" +`, + }, + "KObjSlice nil arg": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice(nil)}, + expectedOutput: `I output.go:] "test" pods="[]" +`, + }, + "KObjSlice int arg": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice(1)}, + expectedOutput: `I output.go:] "test" pods="" +`, + }, + "KObjSlice nil entry": { + text: "test", + values: []interface{}{"pods", + klog.KObjSlice([]interface{}{ + &kmeta{Name: "pod-1", Namespace: "kube-system"}, + nil, + })}, + expectedOutput: `I output.go:] "test" pods="[kube-system/pod-1 ]" +`, + }, + "KObjSlice ints": { + text: "test", + values: []interface{}{"ints", + klog.KObjSlice([]int{1, 2, 3})}, + expectedOutput: `I output.go:] "test" ints="" +`, + }, + "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" +`, + }, + "Error() for nil": { + text: "error nil", + err: (*customErrorJSON)(nil), + expectedOutput: `E output.go:] "error nil" err="" +`, + }, + "String() for nil": { + text: "stringer nil", + values: []interface{}{"stringer", (*stringer)(nil)}, + expectedOutput: `I output.go:] "stringer nil" stringer="" +`, + }, + "MarshalLog() for nil": { + text: "marshaler nil", + values: []interface{}{"obj", (*klog.ObjectRef)(nil)}, + expectedOutput: `I output.go:] "marshaler nil" obj="" +`, + }, + "Error() that panics": { + text: "error panic", + err: faultyError{}, + expectedOutput: `E output.go:] "error panic" err="" +`, + }, + "String() that panics": { + text: "stringer panic", + values: []interface{}{"stringer", faultyStringer{}}, + expectedOutput: `I output.go:] "stringer panic" stringer="" +`, + }, + "MarshalLog() that panics": { + text: "marshaler panic", + values: []interface{}{"obj", faultyMarshaler{}}, + expectedOutput: `I output.go:] "marshaler panic" obj="" +`, + }, + "MarshalLog() that returns itself": { + text: "marshaler recursion", + values: []interface{}{"obj", recursiveMarshaler{}}, + expectedOutput: `I output.go:] "marshaler recursion" obj={} +`, + }, + "handle integer keys": { + withValues: []interface{}{1, "value", 2, "value2"}, + text: "integer keys", + values: []interface{}{"akey", "avalue", "akey2"}, + expectedOutput: `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" +`, + }, + "struct keys": { + withValues: []interface{}{struct{ name string }{"name"}, "value", "test", "other value"}, + text: "struct keys", + values: []interface{}{"key", "val"}, + expectedOutput: `I output.go:] "struct keys" {name}="value" test="other value" key="val" +`, + }, + "map keys": { + withValues: []interface{}{}, + text: "map keys", + values: []interface{}{map[string]bool{"test": true}, "test"}, + expectedOutput: `I output.go:] "map keys" map[test:%!s(bool=true)]="test" +`, + }, +} + +func printWithLogger(logger logr.Logger, test testcase) { + 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...) // + } + } +} + +var _, _, printWithLoggerLine, _ = runtime.Caller(0) // anchor for finding the line numbers above + +func initPrintWithKlog(tb testing.TB, test testcase) { + if test.withHelper && test.vmodule != "" { + tb.Skip("klog does not support -vmodule properly when using helper functions") + } + + state := klog.CaptureState() + tb.Cleanup(state.Restore) + + var fs flag.FlagSet + klog.InitFlags(&fs) + if err := fs.Set("v", "10"); err != nil { + tb.Fatalf("unexpected error: %v", err) + } + if err := fs.Set("vmodule", test.vmodule); err != nil { + tb.Fatalf("unexpected error: %v", err) + } +} + +func printWithKlog(test testcase) { + 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...) + } + } +} + +var _, _, printWithKlogLine, _ = runtime.Caller(0) // anchor for finding the line numbers above + // Output covers various special cases of emitting log output. // It can be used for arbitrary logr.Logger implementations. // @@ -125,370 +508,10 @@ type OutputConfig struct { // 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: "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": { - 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" -`, - }, - "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: "odd arguments", - values: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: `I output.go:] "odd arguments" 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: "both odd", - values: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: `I output.go:] "both odd" 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] -`, - }, - "KObjSlice okay": { - text: "test", - values: []interface{}{"pods", - klog.KObjSlice([]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]" -`, - }, - "KObjSlice nil arg": { - text: "test", - values: []interface{}{"pods", - klog.KObjSlice(nil)}, - expectedOutput: `I output.go:] "test" pods="[]" -`, - }, - "KObjSlice int arg": { - text: "test", - values: []interface{}{"pods", - klog.KObjSlice(1)}, - expectedOutput: `I output.go:] "test" pods="" -`, - }, - "KObjSlice nil entry": { - text: "test", - values: []interface{}{"pods", - klog.KObjSlice([]interface{}{ - &kmeta{Name: "pod-1", Namespace: "kube-system"}, - nil, - })}, - expectedOutput: `I output.go:] "test" pods="[kube-system/pod-1 ]" -`, - }, - "KObjSlice ints": { - text: "test", - values: []interface{}{"ints", - klog.KObjSlice([]int{1, 2, 3})}, - expectedOutput: `I output.go:] "test" ints="" -`, - }, - "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" -`, - }, - "Error() for nil": { - text: "error nil", - err: (*customErrorJSON)(nil), - expectedOutput: `E output.go:] "error nil" err="" -`, - }, - "String() for nil": { - text: "stringer nil", - values: []interface{}{"stringer", (*stringer)(nil)}, - expectedOutput: `I output.go:] "stringer nil" stringer="" -`, - }, - "MarshalLog() for nil": { - text: "marshaler nil", - values: []interface{}{"obj", (*klog.ObjectRef)(nil)}, - expectedOutput: `I output.go:] "marshaler nil" obj="" -`, - }, - "Error() that panics": { - text: "error panic", - err: faultyError{}, - expectedOutput: `E output.go:] "error panic" err="" -`, - }, - "String() that panics": { - text: "stringer panic", - values: []interface{}{"stringer", faultyStringer{}}, - expectedOutput: `I output.go:] "stringer panic" stringer="" -`, - }, - "MarshalLog() that panics": { - text: "marshaler panic", - values: []interface{}{"obj", faultyMarshaler{}}, - expectedOutput: `I output.go:] "marshaler panic" obj="" -`, - }, - "MarshalLog() that returns itself": { - text: "marshaler recursion", - values: []interface{}{"obj", recursiveMarshaler{}}, - expectedOutput: `I output.go:] "marshaler recursion" obj={} -`, - }, - "handle integer keys": { - withValues: []interface{}{1, "value", 2, "value2"}, - text: "integer keys", - values: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: `I output.go:] "integer keys" %!s(int=1)="value" %!s(int=2)="value2" akey="avalue" akey2="(MISSING)" -`, - }, - "struct keys": { - withValues: []interface{}{struct{ name string }{"name"}, "value", "test", "other value"}, - text: "struct keys", - values: []interface{}{"key", "val"}, - expectedOutput: `I output.go:] "struct keys" {name}="value" test="other value" key="val" -`, - }, - "map keys": { - withValues: []interface{}{}, - text: "map keys", - values: []interface{}{map[string]bool{"test": true}, "test"}, - expectedOutput: `I output.go:] "map keys" map[test:%!s(bool=true)]="test" -`, - }, - } 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) - } - // 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) @@ -530,16 +553,16 @@ I output.go:] "test" firstKey=1 secondKey=3 if config.NewLogger == nil { // Test klog. - testOutput(t, printWithKlogLine, func(buffer *bytes.Buffer) { - printWithKlog() + testOutput(t, printWithKlogLine-1, func(buffer *bytes.Buffer) { + printWithKlog(test) }) return } if config.AsBackend { - testOutput(t, printWithKlogLine, func(buffer *bytes.Buffer) { + testOutput(t, printWithKlogLine-1, func(buffer *bytes.Buffer) { klog.SetLogger(config.NewLogger(buffer, 10, "")) - printWithKlog() + printWithKlog(test) }) return } @@ -548,8 +571,8 @@ I output.go:] "test" firstKey=1 secondKey=3 t.Skip("vmodule not supported") } - testOutput(t, printWithLoggerLine, func(buffer *bytes.Buffer) { - printWithLogger(config.NewLogger(buffer, 10, test.vmodule)) + testOutput(t, printWithLoggerLine-1, func(buffer *bytes.Buffer) { + printWithLogger(config.NewLogger(buffer, 10, test.vmodule), test) }) }) } @@ -783,6 +806,55 @@ I output.go:] "test" firstKey=1 secondKey=3 } } +// Benchmark covers various special cases of emitting log output. +// It can be used for arbitrary logr.Logger implementations. +// +// 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 Benchmark(b *testing.B, config OutputConfig) { + for n, test := range tests { + b.Run(n, func(b *testing.B) { + state := klog.CaptureState() + defer state.Restore() + klog.SetOutput(io.Discard) + initPrintWithKlog(b, test) + b.ResetTimer() + + if config.NewLogger == nil { + // Test klog. + for i := 0; i < b.N; i++ { + printWithKlog(test) + } + return + } + + if config.AsBackend { + klog.SetLogger(config.NewLogger(io.Discard, 10, "")) + for i := 0; i < b.N; i++ { + printWithKlog(test) + } + return + } + + if test.vmodule != "" && !config.SupportsVModule { + b.Skip("vmodule not supported") + } + + logger := config.NewLogger(io.Discard, 10, test.vmodule) + b.ResetTimer() + for i := 0; i < b.N; i++ { + printWithLogger(logger, test) + } + }) + } +} + func copySlice(in []interface{}) []interface{} { return append([]interface{}{}, in...) } diff --git a/textlogger/output_test.go b/textlogger/output_test.go index 9a3bc95e3..9aeb0bc27 100644 --- a/textlogger/output_test.go +++ b/textlogger/output_test.go @@ -26,23 +26,34 @@ import ( "k8s.io/klog/v2/textlogger" ) -// TestTextloggerOutput tests the textlogger, directly and as backend. +// These test cover the textlogger, directly and as backend. +func newLogger(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) +} + +var ( + directConfig = test.OutputConfig{NewLogger: newLogger, SupportsVModule: true} + indirectConfig = test.OutputConfig{NewLogger: newLogger, AsBackend: true} +) + func TestTextloggerOutput(t *testing.T) { test.InitKlog(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}) + test.Output(t, directConfig) }) t.Run("klog-backend", func(t *testing.T) { - test.Output(t, test.OutputConfig{NewLogger: newLogger, AsBackend: true}) + test.Output(t, indirectConfig) }) } + +func BenchmarkTextloggerOutput(b *testing.B) { + test.InitKlog(b) + test.Benchmark(b, directConfig) +} From ff4f80fcbad7920bd92d07b63d23267f2ddd061a Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 16 Jan 2023 11:25:47 +0100 Subject: [PATCH 111/125] test: unit tests for vmodule support Text logger and the two klogr implementations both got this wrong. --- test/output.go | 24 ++++++++++++++++++++---- test/output_helper.go | 4 ++-- test/zapr.go | 5 +++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/test/output.go b/test/output.go index 16553f2b4..bfd026c2f 100644 --- a/test/output.go +++ b/test/output.go @@ -153,12 +153,28 @@ var tests = map[string]testcase{ text: "v=11: you see me because of -vmodule output=11", v: 11, vmodule: "output=11", + expectedOutput: `I output.go:] "v=11: you see me because of -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", }, + "vmodule with helper": { + text: "v=11: you see me because of -vmodule output=11", + withHelper: true, + v: 11, + vmodule: "output=11", + expectedOutput: `I output.go:] "v=11: you see me because of -vmodule output=11" +`, + }, + "other vmodule with helper": { + text: "v=11: you still don't see me because of -vmodule output_helper=11", + withHelper: true, + v: 11, + vmodule: "output_helper=11", + }, "log with name and values": { withNames: []string{"me"}, text: "test", @@ -410,7 +426,7 @@ func printWithLogger(logger logr.Logger, test testcase) { } for _, logger := range loggers { if test.withHelper { - loggerHelper(logger, test.text, test.values) // + loggerHelper(logger.V(test.v), test.text, test.values) // } else if test.err != nil { logger.Error(test.err, test.text, test.values...) // } else { @@ -480,7 +496,7 @@ func printWithKlog(test testcase) { text = strings.Join(test.withNames, "/") + ": " + text } if test.withHelper { - klogHelper(text, kv) + klogHelper(klog.Level(test.v), text, kv) } else if test.err != nil { klog.ErrorS(test.err, text, kv...) } else { @@ -510,7 +526,7 @@ var _, _, printWithKlogLine, _ = runtime.Caller(0) // anchor for finding the lin func Output(t *testing.T, config OutputConfig) { for n, test := range tests { t.Run(n, func(t *testing.T) { - defer klog.ClearLogger() + initPrintWithKlog(t, test) testOutput := func(t *testing.T, expectedLine int, print func(buffer *bytes.Buffer)) { var tmpWriteBuffer bytes.Buffer @@ -561,7 +577,7 @@ func Output(t *testing.T, config OutputConfig) { if config.AsBackend { testOutput(t, printWithKlogLine-1, func(buffer *bytes.Buffer) { - klog.SetLogger(config.NewLogger(buffer, 10, "")) + klog.SetLogger(config.NewLogger(buffer, 10, test.vmodule)) printWithKlog(test) }) return diff --git a/test/output_helper.go b/test/output_helper.go index 499395e38..58b7348f9 100644 --- a/test/output_helper.go +++ b/test/output_helper.go @@ -27,6 +27,6 @@ func loggerHelper(logger logr.Logger, msg string, kv []interface{}) { logger.Info(msg, kv...) } -func klogHelper(msg string, kv []interface{}) { - klog.InfoSDepth(1, msg, kv...) +func klogHelper(level klog.Level, msg string, kv []interface{}) { + klog.V(level).InfoSDepth(1, msg, kv...) } diff --git a/test/zapr.go b/test/zapr.go index 9f6cd8b4c..48cf2c3da 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -319,6 +319,11 @@ I output.go:] "test" firstKey=1 secondKey=3 `: `{"caller":"test/output.go:","msg":"non-string key argument passed to logging, ignoring all later arguments","invalid key":{"test":true}} {"caller":"test/output.go:","msg":"map keys","v":0} `, + + // zapr does not support vmodule checks and thus always + // discards these messages. + `I output.go:] "v=11: you see me because of -vmodule output=11" +`: ``, } { mapping[key] = value } From 1a1367cc64e28f8bf3dcc04cd74010cc67bdd57d Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 12:43:34 +0100 Subject: [PATCH 112/125] buffer: use sync.Pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This simplifies the code. Instead of different instances, the package now maintains a global pool. This makes the text logger struct a bit smaller and thus cheaper to copy in the With* functions. Performance is about the same as before: name old time/op new time/op delta Header-36 1.68µs ± 7% 1.62µs ± 6% ~ (p=0.246 n=5+5) HeaderWithDir-36 1.63µs ± 6% 1.59µs ± 6% ~ (p=0.690 n=5+5) name old alloc/op new alloc/op delta Header-36 216B ± 0% 216B ± 0% ~ (all equal) HeaderWithDir-36 216B ± 0% 216B ± 0% ~ (all equal) name old allocs/op new allocs/op delta Header-36 2.00 ± 0% 2.00 ± 0% ~ (all equal) HeaderWithDir-36 2.00 ± 0% 2.00 ± 0% ~ (all equal) The text logger didn't actually return the buffer. Now it does. --- internal/buffer/buffer.go | 40 +++++++++------------------------------ klog.go | 29 ++++++++++++---------------- klog_test.go | 4 ++-- textlogger/textlogger.go | 19 +++++++++---------- 4 files changed, 32 insertions(+), 60 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index ac88682a2..d53b49da3 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -40,44 +40,22 @@ type Buffer struct { 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 +var buffers = sync.Pool{ + New: func() interface{} { + return new(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() - } +func GetBuffer() *Buffer { + b := buffers.Get().(*Buffer) + 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() +func PutBuffer(b *Buffer) { + buffers.Put(b) } // Some custom tiny helper functions to print the log header efficiently. diff --git a/klog.go b/klog.go index bcf60cf97..c5d98ad38 100644 --- a/klog.go +++ b/klog.go @@ -532,11 +532,6 @@ func (s settings) deepCopy() settings { type loggingT struct { settings - // 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. - bufferCache buffer.Buffers - // flushD holds a flushDaemon that frequently flushes log file buffers. // Uses its own mutex. flushD *flushDaemon @@ -664,7 +659,7 @@ func (l *loggingT) header(s severity.Severity, depth int) (*buffer.Buffer, strin // formatHeader formats a log header using the provided file name and line number. func (l *loggingT) formatHeader(s severity.Severity, file string, line int) *buffer.Buffer { - buf := l.bufferCache.GetBuffer() + buf := buffer.GetBuffer() if l.skipHeaders { return buf } @@ -682,8 +677,8 @@ func (l *loggingT) printlnDepth(s severity.Severity, logger *logr.Logger, filter // if logger is set, we clear the generated header as we rely on the backing // logger implementation to print headers if logger != nil { - l.bufferCache.PutBuffer(buf) - buf = l.bufferCache.GetBuffer() + buffer.PutBuffer(buf) + buf = buffer.GetBuffer() } if filter != nil { args = filter.Filter(args) @@ -701,8 +696,8 @@ func (l *loggingT) printDepth(s severity.Severity, logger *logr.Logger, filter L // if logr is set, we clear the generated header as we rely on the backing // logr implementation to print headers if logger != nil { - l.bufferCache.PutBuffer(buf) - buf = l.bufferCache.GetBuffer() + buffer.PutBuffer(buf) + buf = buffer.GetBuffer() } if filter != nil { args = filter.Filter(args) @@ -723,8 +718,8 @@ func (l *loggingT) printfDepth(s severity.Severity, logger *logr.Logger, filter // if logr is set, we clear the generated header as we rely on the backing // logr implementation to print headers if logger != nil { - l.bufferCache.PutBuffer(buf) - buf = l.bufferCache.GetBuffer() + buffer.PutBuffer(buf) + buf = buffer.GetBuffer() } if filter != nil { format, args = filter.FilterF(format, args) @@ -744,8 +739,8 @@ func (l *loggingT) printWithFileLine(s severity.Severity, logger *logr.Logger, f // if logr is set, we clear the generated header as we rely on the backing // logr implementation to print headers if logger != nil { - l.bufferCache.PutBuffer(buf) - buf = l.bufferCache.GetBuffer() + buffer.PutBuffer(buf) + buf = buffer.GetBuffer() } if filter != nil { args = filter.Filter(args) @@ -785,7 +780,7 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s // set log severity by s 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.bufferCache.GetBuffer() + b := buffer.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. @@ -796,7 +791,7 @@ func (l *loggingT) printS(err error, s severity.Severity, depth int, msg string, serialize.KVListFormat(&b.Buffer, keysAndValues...) l.printDepth(s, logging.logger, nil, depth+1, &b.Buffer) // Make the buffer available for reuse. - l.bufferCache.PutBuffer(b) + buffer.PutBuffer(b) } // redirectBuffer is used to set an alternate destination for the logs @@ -948,7 +943,7 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf timeoutFlush(ExitFlushTimeout) OsExit(255) // C++ uses -1, which is silly because it's anded with 255 anyway. } - l.bufferCache.PutBuffer(buf) + buffer.PutBuffer(buf) if stats := severityStats[s]; stats != nil { atomic.AddInt64(&stats.lines, 1) diff --git a/klog_test.go b/klog_test.go index e6e8922cc..96f337ac5 100644 --- a/klog_test.go +++ b/klog_test.go @@ -623,7 +623,7 @@ func TestLogBacktraceAt(t *testing.T) { func BenchmarkHeader(b *testing.B) { for i := 0; i < b.N; i++ { buf, _, _ := logging.header(severity.InfoLog, 0) - logging.bufferCache.PutBuffer(buf) + buffer.PutBuffer(buf) } } @@ -631,7 +631,7 @@ func BenchmarkHeaderWithDir(b *testing.B) { logging.addDirHeader = true for i := 0; i < b.N; i++ { buf, _, _ := logging.header(severity.InfoLog, 0) - logging.bufferCache.PutBuffer(buf) + buffer.PutBuffer(buf) } } diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 3c6cda645..360888cc8 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -56,19 +56,17 @@ var ( // 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{}, + prefix: "", + values: nil, + config: c, }) } type tlogger struct { - callDepth int - prefix string - values []interface{} - config *Config - bufferCache *buffer.Buffers + callDepth int + prefix string + values []interface{} + config *Config } func copySlice(in []interface{}) []interface{} { @@ -103,7 +101,8 @@ func (l *tlogger) Error(err error, msg string, kvList ...interface{}) { 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() + b := buffer.GetBuffer() + defer buffer.PutBuffer(b) // Determine caller. // +1 for this frame, +1 for Info/Error. From 9674caedee315c56c6f988e5fe5738d0401d7f79 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 25 Nov 2022 09:40:17 +0100 Subject: [PATCH 113/125] textlogger: remove some dead code --- textlogger/textlogger.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 360888cc8..203d5b9ab 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -69,12 +69,6 @@ type tlogger struct { config *Config } -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 } From 926ab6d62180488eed04bfc30483e48541b00e47 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 16 Dec 2022 14:56:07 +0100 Subject: [PATCH 114/125] serialize: combine merging and formatting This avoids one memory allocation (for the intermediate slice), copying and loops. --- internal/serialize/keyvalues.go | 163 +++++++++++++++++++++----------- ktesting/testinglogger.go | 8 +- textlogger/textlogger.go | 5 +- 3 files changed, 112 insertions(+), 64 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index ad6bf1116..0bff64d04 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -91,6 +91,51 @@ func MergeKVs(first, second []interface{}) []interface{} { return merged } +// MergeKVsInto is a variant of MergeKVs which directly formats the key/value +// pairs into a buffer. +func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { + if len(first) == 0 && len(second) == 0 { + // Nothing to do at all. + return + } + + if len(first) == 0 && len(second)%2 == 0 { + // Nothing to be overridden, second slice is well-formed + // and can be used directly. + for i := 0; i < len(second); i += 2 { + KVFormat(b, second[i], second[i+1]) + } + return + } + + // Determine which keys are in the second slice so that we can skip + // them when iterating over the first one. The code intentionally + // favors performance over completeness: we assume that keys are string + // constants and thus compare equal when the string values are equal. A + // string constant being overridden by, for example, a fmt.Stringer is + // not handled. + overrides := map[interface{}]bool{} + for i := 0; i < len(second); i += 2 { + overrides[second[i]] = true + } + for i := 0; i < len(first); i += 2 { + key := first[i] + if overrides[key] { + continue + } + KVFormat(b, key, first[i+1]) + } + // Round down. + l := len(second) + l = l / 2 * 2 + for i := 1; i < l; i += 2 { + KVFormat(b, second[i-1], second[i]) + } + if len(second)%2 == 1 { + KVFormat(b, second[len(second)-1], missingValue) + } +} + const missingValue = "(MISSING)" // KVListFormat serializes all key/value pairs into the provided buffer. @@ -104,66 +149,72 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { } 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 sK, ok := k.(string); ok { - // Avoid one allocation when the key is a string, which - // normally it should be. - b.WriteString(sK) - } else { - b.WriteString(fmt.Sprintf("%s", k)) - } + KVFormat(b, k, v) + } +} - // 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)) +// KVFormat serializes one key/value pair into the provided buffer. +// A space gets inserted before the pair. +func KVFormat(b *bytes.Buffer, k, v interface{}) { + 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 sK, ok := k.(string); ok { + // Avoid one allocation when the key is a string, which + // normally it should be. + b.WriteString(sK) + } 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, ErrorToString(v)) + case logr.Marshaler: + value := MarshalerToValue(v) + // A marshaler that returns a string is useful for + // delayed formatting of complex values. We treat this + // case like a normal string. This is useful for + // multi-line support. + // + // We could do this by recursively formatting a value, + // but that comes with the risk of infinite recursion + // if a marshaler returns itself. Instead we call it + // only once and rely on it returning the intended + // value directly. + switch value := value.(type) { case string: - writeStringValue(b, true, v) - case error: - writeStringValue(b, true, ErrorToString(v)) - case logr.Marshaler: - value := MarshalerToValue(v) - // A marshaler that returns a string is useful for - // delayed formatting of complex values. We treat this - // case like a normal string. This is useful for - // multi-line support. - // - // We could do this by recursively formatting a value, - // but that comes with the risk of infinite recursion - // if a marshaler returns itself. Instead we call it - // only once and rely on it returning the intended - // value directly. - switch value := value.(type) { - case string: - writeStringValue(b, true, value) - default: - writeStringValue(b, false, fmt.Sprintf("%+v", value)) - } - 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)) + writeStringValue(b, true, value) default: - writeStringValue(b, false, fmt.Sprintf("%+v", v)) + writeStringValue(b, false, fmt.Sprintf("%+v", value)) } + 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)) } } diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 67bbd857f..0c87a953b 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -319,8 +319,7 @@ func (l tlogger) Info(level int, msg string, kvList ...interface{}) { l.shared.t.Helper() buffer := &bytes.Buffer{} - merged := serialize.MergeKVs(l.values, kvList) - serialize.KVListFormat(buffer, merged...) + serialize.MergeAndFormatKVs(buffer, l.values, kvList) l.log(LogInfo, msg, level, buffer, nil, kvList) } @@ -339,10 +338,9 @@ func (l tlogger) Error(err error, msg string, kvList ...interface{}) { l.shared.t.Helper() buffer := &bytes.Buffer{} if err != nil { - serialize.KVListFormat(buffer, "err", err) + serialize.KVFormat(buffer, "err", err) } - merged := serialize.MergeKVs(l.values, kvList) - serialize.KVListFormat(buffer, merged...) + serialize.MergeAndFormatKVs(buffer, l.values, kvList) l.log(LogError, msg, 0, buffer, err, kvList) } diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index 203d5b9ab..be946e0f5 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -125,10 +125,9 @@ func (l *tlogger) print(err error, s severity.Severity, msg string, kvList []int // message and put the multi-line output into a value. b.WriteString(strconv.Quote(msg)) if err != nil { - serialize.KVListFormat(&b.Buffer, "err", err) + serialize.KVFormat(&b.Buffer, "err", err) } - merged := serialize.MergeKVs(l.values, kvList) - serialize.KVListFormat(&b.Buffer, merged...) + serialize.MergeAndFormatKVs(&b.Buffer, l.values, kvList) if b.Len() == 0 || b.Bytes()[b.Len()-1] != '\n' { b.WriteByte('\n') } From a1638bf6333adeb9e466a8af27ac3e926c31d2e1 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 22 Dec 2022 17:09:45 +0100 Subject: [PATCH 115/125] KObj: optimize string concatenation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using a strings.Builder reduces the number of allocations: name old time/op new time/op delta KlogOutput/KObjSlice_okay-36 15.2µs ± 5% 14.8µs ± 4% ~ (p=0.151 n=5+5) KlogOutput/KObjSlice_nil_entry-36 14.4µs ± 5% 13.6µs ± 3% -5.25% (p=0.032 n=5+5) KlogOutput/KObj-36 13.5µs ± 8% 13.5µs ± 6% ~ (p=0.841 n=5+5) KlogOutput/KObjSlice_ints-36 15.3µs ± 5% 15.2µs ± 4% ~ (p=0.841 n=5+5) KlogOutput/KObjSlice_nil_arg-36 12.8µs ± 2% 12.8µs ± 6% ~ (p=0.841 n=5+5) KlogOutput/KObjSlice_int_arg-36 14.1µs ± 4% 13.8µs ± 3% ~ (p=0.310 n=5+5) KlogOutput/KObjs-36 14.1µs ± 8% 13.6µs ± 8% ~ (p=0.690 n=5+5) name old alloc/op new alloc/op delta KlogOutput/KObjSlice_okay-36 2.89kB ± 0% 2.82kB ± 0% -2.23% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_entry-36 2.65kB ± 0% 2.62kB ± 0% ~ (p=0.079 n=4+5) KlogOutput/KObj-36 2.50kB ± 0% 2.47kB ± 0% -1.30% (p=0.000 n=4+5) KlogOutput/KObjSlice_ints-36 2.90kB ± 0% 2.90kB ± 0% ~ (p=1.000 n=5+5) KlogOutput/KObjSlice_nil_arg-36 2.41kB ± 0% 2.41kB ± 0% ~ (all equal) KlogOutput/KObjSlice_int_arg-36 2.67kB ± 0% 2.67kB ± 0% ~ (all equal) KlogOutput/KObjs-36 2.72kB ± 0% 2.65kB ± 0% -2.38% (p=0.008 n=5+5) name old allocs/op new allocs/op delta KlogOutput/KObjSlice_okay-36 46.0 ± 0% 42.0 ± 0% -8.70% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_entry-36 40.0 ± 0% 38.0 ± 0% -5.00% (p=0.008 n=5+5) KlogOutput/KObj-36 36.0 ± 0% 34.0 ± 0% -5.56% (p=0.008 n=5+5) KlogOutput/KObjSlice_ints-36 39.0 ± 0% 39.0 ± 0% ~ (all equal) KlogOutput/KObjSlice_nil_arg-36 35.0 ± 0% 35.0 ± 0% ~ (all equal) KlogOutput/KObjSlice_int_arg-36 37.0 ± 0% 37.0 ± 0% ~ (all equal) KlogOutput/KObjs-36 42.0 ± 0% 38.0 ± 0% -9.52% (p=0.008 n=5+5) --- k8s_references.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/k8s_references.go b/k8s_references.go index 2c218f698..a6436b5d1 100644 --- a/k8s_references.go +++ b/k8s_references.go @@ -19,6 +19,7 @@ package klog import ( "fmt" "reflect" + "strings" "github.com/go-logr/logr" ) @@ -31,7 +32,12 @@ type ObjectRef struct { func (ref ObjectRef) String() string { if ref.Namespace != "" { - return fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) + var builder strings.Builder + builder.Grow(len(ref.Namespace) + len(ref.Name) + 1) + builder.WriteString(ref.Namespace) + builder.WriteRune('/') + builder.WriteString(ref.Name) + return builder.String() } return ref.Name } From 8eadf2c888dd82a1b3dc0a68afb4562fe6144740 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 22 Dec 2022 18:19:13 +0100 Subject: [PATCH 116/125] KObj, KObjSlice: format more efficiently as text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The klog text format avoids some string allocation by calling WriteText instead of String when encountering such values. A positive side effect is that KObjSlice now gets logged like KObjs, i.e. pods=[namespace1/name1 namespace2/name2] Previously, it was written as a quoted string. To achieve the best performance, WriteText is passed a bytes.Buffer pointer instead of a io.Writer. This avoids an interface allocation for the parameter and provides access to the underlying methods. Benchmarks involving these types benefit while other are the same as before: name old time/op new time/op delta KlogOutput/KObj-36 12.7µs ±10% 13.1µs ± 1% ~ (p=0.151 n=5+5) KlogOutput/KObjs-36 13.4µs ± 7% 14.0µs ± 5% ~ (p=0.310 n=5+5) KlogOutput/KObjSlice_okay-36 14.8µs ± 4% 13.0µs ± 3% -12.33% (p=0.008 n=5+5) KlogOutput/KObjSlice_int_arg-36 14.0µs ± 6% 12.3µs ±10% -12.60% (p=0.008 n=5+5) KlogOutput/KObjSlice_ints-36 15.5µs ± 4% 12.8µs ± 6% -17.85% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_entry-36 14.2µs ±13% 12.6µs ± 3% -11.39% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_arg-36 12.6µs ± 6% 12.9µs ± 3% ~ (p=0.690 n=5+5) name old alloc/op new alloc/op delta KlogOutput/KObj-36 2.47kB ± 0% 2.41kB ± 0% -2.29% (p=0.008 n=5+5) KlogOutput/KObjs-36 2.65kB ± 0% 2.65kB ± 0% ~ (all equal) KlogOutput/KObjSlice_okay-36 2.82kB ± 0% 2.45kB ± 0% -13.37% (p=0.008 n=5+5) KlogOutput/KObjSlice_int_arg-36 2.67kB ± 0% 2.45kB ± 0% -8.42% (p=0.029 n=4+4) KlogOutput/KObjSlice_ints-36 2.90kB ± 0% 2.49kB ± 0% -14.37% (p=0.000 n=4+5) KlogOutput/KObjSlice_nil_entry-36 2.62kB ± 0% 2.43kB ± 0% -7.34% (p=0.000 n=4+5) KlogOutput/KObjSlice_nil_arg-36 2.41kB ± 0% 2.40kB ± 0% -0.66% (p=0.016 n=5+4) name old allocs/op new allocs/op delta KlogOutput/KObj-36 34.0 ± 0% 32.0 ± 0% -5.88% (p=0.008 n=5+5) KlogOutput/KObjs-36 38.0 ± 0% 38.0 ± 0% ~ (all equal) KlogOutput/KObjSlice_okay-36 42.0 ± 0% 32.0 ± 0% -23.81% (p=0.008 n=5+5) KlogOutput/KObjSlice_int_arg-36 37.0 ± 0% 32.0 ± 0% -13.51% (p=0.008 n=5+5) KlogOutput/KObjSlice_ints-36 39.0 ± 0% 33.0 ± 0% -15.38% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_entry-36 38.0 ± 0% 32.0 ± 0% -15.79% (p=0.008 n=5+5) KlogOutput/KObjSlice_nil_arg-36 35.0 ± 0% 32.0 ± 0% -8.57% (p=0.008 n=5+5) --- internal/serialize/keyvalues.go | 16 ++++++++ k8s_references.go | 70 +++++++++++++++++++++++++++------ test/output.go | 10 ++--- test/zapr.go | 12 ++---- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index 0bff64d04..f9558c3d2 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -24,6 +24,10 @@ import ( "github.com/go-logr/logr" ) +type textWriter interface { + WriteText(*bytes.Buffer) +} + // 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. @@ -175,6 +179,8 @@ func KVFormat(b *bytes.Buffer, k, v interface{}) { // than plain strings // (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235). switch v := v.(type) { + case textWriter: + writeTextWriterValue(b, v) case fmt.Stringer: writeStringValue(b, true, StringerToString(v)) case string: @@ -254,6 +260,16 @@ func ErrorToString(err error) (ret string) { return } +func writeTextWriterValue(b *bytes.Buffer, v textWriter) { + b.WriteRune('=') + defer func() { + if err := recover(); err != nil { + fmt.Fprintf(b, `""`, err) + } + }() + v.WriteText(b) +} + func writeStringValue(b *bytes.Buffer, quote bool, v string) { data := []byte(v) index := bytes.IndexByte(data, '\n') diff --git a/k8s_references.go b/k8s_references.go index a6436b5d1..ecd3f8b69 100644 --- a/k8s_references.go +++ b/k8s_references.go @@ -17,6 +17,7 @@ limitations under the License. package klog import ( + "bytes" "fmt" "reflect" "strings" @@ -42,6 +43,20 @@ func (ref ObjectRef) String() string { return ref.Name } +func (ref ObjectRef) WriteText(out *bytes.Buffer) { + out.WriteRune('"') + ref.writeUnquoted(out) + out.WriteRune('"') +} + +func (ref ObjectRef) writeUnquoted(out *bytes.Buffer) { + if ref.Namespace != "" { + out.WriteString(ref.Namespace) + out.WriteRune('/') + } + out.WriteString(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{} { @@ -123,31 +138,31 @@ var _ fmt.Stringer = kobjSlice{} var _ logr.Marshaler = kobjSlice{} func (ks kobjSlice) String() string { - objectRefs, err := ks.process() - if err != nil { - return err.Error() + objectRefs, errStr := ks.process() + if errStr != "" { + return errStr } return fmt.Sprintf("%v", objectRefs) } func (ks kobjSlice) MarshalLog() interface{} { - objectRefs, err := ks.process() - if err != nil { - return err.Error() + objectRefs, errStr := ks.process() + if errStr != "" { + return errStr } return objectRefs } -func (ks kobjSlice) process() ([]interface{}, error) { +func (ks kobjSlice) process() (objs []interface{}, err string) { s := reflect.ValueOf(ks.arg) switch s.Kind() { case reflect.Invalid: // nil parameter, print as nil. - return nil, nil + return nil, "" case reflect.Slice: // Okay, handle below. default: - return nil, fmt.Errorf("", ks.arg) + return nil, fmt.Sprintf("", ks.arg) } objectRefs := make([]interface{}, 0, s.Len()) for i := 0; i < s.Len(); i++ { @@ -157,8 +172,41 @@ func (ks kobjSlice) process() ([]interface{}, error) { } else if v, ok := item.(KMetadata); ok { objectRefs = append(objectRefs, KObj(v)) } else { - return nil, fmt.Errorf("", item) + return nil, fmt.Sprintf("", item) + } + } + return objectRefs, "" +} + +var nilToken = []byte("") + +func (ks kobjSlice) WriteText(out *bytes.Buffer) { + s := reflect.ValueOf(ks.arg) + switch s.Kind() { + case reflect.Invalid: + // nil parameter, print as empty slice. + out.WriteString("[]") + return + case reflect.Slice: + // Okay, handle below. + default: + fmt.Fprintf(out, `""`, ks.arg) + return + } + out.Write([]byte{'['}) + defer out.Write([]byte{']'}) + for i := 0; i < s.Len(); i++ { + if i > 0 { + out.Write([]byte{' '}) + } + item := s.Index(i).Interface() + if item == nil { + out.Write(nilToken) + } else if v, ok := item.(KMetadata); ok { + KObj(v).writeUnquoted(out) + } else { + fmt.Fprintf(out, "", item) + return } } - return objectRefs, nil } diff --git a/test/output.go b/test/output.go index bfd026c2f..4dc52f766 100644 --- a/test/output.go +++ b/test/output.go @@ -290,14 +290,14 @@ I output.go:] "test" firstKey=1 secondKey=3 &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]" + expectedOutput: `I output.go:] "test" pods=[kube-system/pod-1 kube-system/pod-2] `, }, "KObjSlice nil arg": { text: "test", values: []interface{}{"pods", klog.KObjSlice(nil)}, - expectedOutput: `I output.go:] "test" pods="[]" + expectedOutput: `I output.go:] "test" pods=[] `, }, "KObjSlice int arg": { @@ -314,14 +314,14 @@ I output.go:] "test" firstKey=1 secondKey=3 &kmeta{Name: "pod-1", Namespace: "kube-system"}, nil, })}, - expectedOutput: `I output.go:] "test" pods="[kube-system/pod-1 ]" + expectedOutput: `I output.go:] "test" pods=[kube-system/pod-1 ] `, }, "KObjSlice ints": { text: "test", values: []interface{}{"ints", klog.KObjSlice([]int{1, 2, 3})}, - expectedOutput: `I output.go:] "test" ints="" + expectedOutput: `I output.go:] "test" ints=[] `, }, "regular error types as value": { @@ -357,7 +357,7 @@ I output.go:] "test" firstKey=1 secondKey=3 "MarshalLog() for nil": { text: "marshaler nil", values: []interface{}{"obj", (*klog.ObjectRef)(nil)}, - expectedOutput: `I output.go:] "marshaler nil" obj="" + expectedOutput: `I output.go:] "marshaler nil" obj="" `, }, "Error() that panics": { diff --git a/test/zapr.go b/test/zapr.go index 48cf2c3da..680e022db 100644 --- a/test/zapr.go +++ b/test/zapr.go @@ -73,11 +73,7 @@ func ZaprOutputMappingDirect() map[string]string { `: `{"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" 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" pods="[]" + `I output.go:] "test" pods=[] `: `{"caller":"test/output.go:","msg":"test","v":0,"pods":null} `, @@ -85,11 +81,11 @@ func ZaprOutputMappingDirect() map[string]string { `: `{"caller":"test/output.go:","msg":"test","v":0,"pods":""} `, - `I output.go:] "test" ints="" + `I output.go:] "test" ints=[] `: `{"caller":"test/output.go:","msg":"test","v":0,"ints":""} `, - `I output.go:] "test" pods="[kube-system/pod-1 ]" + `I output.go:] "test" pods=[kube-system/pod-1 ] `: `{"caller":"test/output.go:","msg":"test","v":0,"pods":[{"name":"pod-1","namespace":"kube-system"},null]} `, @@ -140,7 +136,7 @@ I output.go:] "odd WithValues" keyWithoutValue="(MISSING)" {"caller":"test/output.go:","msg":"both odd","basekey1":"basevar1","v":0,"akey":"avalue"} `, - `I output.go:] "marshaler nil" obj="" + `I output.go:] "marshaler nil" obj="" `: `{"caller":"test/output.go:","msg":"marshaler nil","v":0,"objError":"PANIC=value method k8s.io/klog/v2.ObjectRef.MarshalLog called using nil *ObjectRef pointer"} `, From 10250558e88d378cb43e5c8e8337122a3c276edc Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 16 Jan 2023 11:42:45 +0100 Subject: [PATCH 117/125] github: bump golangci-lint image version It looks like golangci-lint comes with a fixed version of Go, one which is too old for the code: level=error msg="Running error: gofmt: analysis skipped: errors in package: [/go/src/k8s.io/klog/test/output.go:846:22: Discard not declared by package io /go/src/k8s.io/klog/test/output.go:864:40: Discard not declared by package io /go/src/k8s.io/klog/test/output.go:876:35: Discard not declared by package io]" io.Discard was introduced in Go 1.16, using it should be fine and is reflected in our Go version test matrix (>= 1.17). --- .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 cca0121f3..870d1a2f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Lint run: | docker run --rm -v `pwd`:/go/src/k8s.io/klog -w /go/src/k8s.io/klog \ - golangci/golangci-lint:v1.23.8 golangci-lint run --disable-all -v \ + golangci/golangci-lint:v1.50.1 golangci-lint run --disable-all -v \ -E govet -E misspell -E gofmt -E ineffassign -E golint apidiff: runs-on: ubuntu-latest From 81f97ff8551e91cd32fbe508e2525db0c67da203 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 24 Jan 2023 10:03:47 +0100 Subject: [PATCH 118/125] klog: support writing formatted buffer through logger This is meant to be used with textlogger for the non-structured log calls to avoid double encoding. --- contextual.go | 30 ++++++++++++++-- klog.go | 97 ++++++++++++++++++++++++++++----------------------- klog_test.go | 2 +- 3 files changed, 83 insertions(+), 46 deletions(-) diff --git a/contextual.go b/contextual.go index 2428963c0..005513f2a 100644 --- a/contextual.go +++ b/contextual.go @@ -70,11 +70,14 @@ func SetLogger(logger logr.Logger) { // routing log entries through klogr into klog and then into the actual Logger // backend. func SetLoggerWithOptions(logger logr.Logger, opts ...LoggerOption) { - logging.logger = &logger logging.loggerOptions = loggerOptions{} for _, opt := range opts { opt(&logging.loggerOptions) } + logging.logger = &logWriter{ + Logger: logger, + writeKlogBuffer: logging.loggerOptions.writeKlogBuffer, + } } // ContextualLogger determines whether the logger passed to @@ -93,6 +96,22 @@ func FlushLogger(flush func()) LoggerOption { } } +// WriteKlogBuffer sets a callback that will be invoked by klog to write output +// produced by non-structured log calls like Infof. +// +// The buffer will contain exactly the same data that klog normally would write +// into its own output stream(s). In particular this includes the header, if +// klog is configured to write one. The callback then can divert that data into +// its own output streams. The buffer may or may not end in a line break. +// +// Without such a callback, klog will call the logger's Info or Error method +// with just the message string (i.e. no header). +func WriteKlogBuffer(write func([]byte)) LoggerOption { + return func(o *loggerOptions) { + o.writeKlogBuffer = write + } +} + // LoggerOption implements the functional parameter paradigm for // SetLoggerWithOptions. type LoggerOption func(o *loggerOptions) @@ -100,6 +119,13 @@ type LoggerOption func(o *loggerOptions) type loggerOptions struct { contextualLogger bool flush func() + writeKlogBuffer func([]byte) +} + +// logWriter combines a logger (always set) with a write callback (optional). +type logWriter struct { + Logger + writeKlogBuffer func([]byte) } // ClearLogger removes a backing Logger implementation if one was set earlier @@ -152,7 +178,7 @@ func Background() Logger { if logging.loggerOptions.contextualLogger { // Is non-nil because logging.loggerOptions.contextualLogger is // only true if a logger was set. - return *logging.logger + return logging.logger.Logger } return klogLogger diff --git a/klog.go b/klog.go index c5d98ad38..466eeaf26 100644 --- a/klog.go +++ b/klog.go @@ -91,8 +91,6 @@ import ( "sync/atomic" "time" - "github.com/go-logr/logr" - "k8s.io/klog/v2/internal/buffer" "k8s.io/klog/v2/internal/clock" "k8s.io/klog/v2/internal/dbg" @@ -453,7 +451,7 @@ type settings struct { // logger is the global Logger chosen by users of klog, nil if // none is available. - logger *Logger + logger *logWriter // loggerOptions contains the options that were supplied for // globalLogger. @@ -525,6 +523,11 @@ func (s settings) deepCopy() settings { } s.vmodule.filter = filter + if s.logger != nil { + logger := *s.logger + s.logger = &logger + } + return s } @@ -668,15 +671,16 @@ func (l *loggingT) formatHeader(s severity.Severity, file string, line int) *buf return buf } -func (l *loggingT) println(s severity.Severity, logger *logr.Logger, filter LogFilter, args ...interface{}) { +func (l *loggingT) println(s severity.Severity, logger *logWriter, filter LogFilter, args ...interface{}) { l.printlnDepth(s, logger, filter, 1, args...) } -func (l *loggingT) printlnDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) { +func (l *loggingT) printlnDepth(s severity.Severity, logger *logWriter, 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 { + // If a logger is set and doesn't support writing a formatted buffer, + // we clear the generated header as we rely on the backing + // logger implementation to print headers. + if logger != nil && logger.writeKlogBuffer == nil { buffer.PutBuffer(buf) buf = buffer.GetBuffer() } @@ -687,15 +691,16 @@ func (l *loggingT) printlnDepth(s severity.Severity, logger *logr.Logger, filter l.output(s, logger, buf, depth, file, line, false) } -func (l *loggingT) print(s severity.Severity, logger *logr.Logger, filter LogFilter, args ...interface{}) { +func (l *loggingT) print(s severity.Severity, logger *logWriter, filter LogFilter, args ...interface{}) { l.printDepth(s, logger, filter, 1, args...) } -func (l *loggingT) printDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, args ...interface{}) { +func (l *loggingT) printDepth(s severity.Severity, logger *logWriter, 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 { + // If a logger is set and doesn't support writing a formatted buffer, + // we clear the generated header as we rely on the backing + // logger implementation to print headers. + if logger != nil && logger.writeKlogBuffer == nil { buffer.PutBuffer(buf) buf = buffer.GetBuffer() } @@ -709,15 +714,16 @@ func (l *loggingT) printDepth(s severity.Severity, logger *logr.Logger, filter L l.output(s, logger, buf, depth, file, line, false) } -func (l *loggingT) printf(s severity.Severity, logger *logr.Logger, filter LogFilter, format string, args ...interface{}) { +func (l *loggingT) printf(s severity.Severity, logger *logWriter, filter LogFilter, format string, args ...interface{}) { l.printfDepth(s, logger, filter, 1, format, args...) } -func (l *loggingT) printfDepth(s severity.Severity, logger *logr.Logger, filter LogFilter, depth int, format string, args ...interface{}) { +func (l *loggingT) printfDepth(s severity.Severity, logger *logWriter, 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 { + // If a logger is set and doesn't support writing a formatted buffer, + // we clear the generated header as we rely on the backing + // logger implementation to print headers. + if logger != nil && logger.writeKlogBuffer == nil { buffer.PutBuffer(buf) buf = buffer.GetBuffer() } @@ -734,11 +740,12 @@ func (l *loggingT) printfDepth(s severity.Severity, logger *logr.Logger, filter // 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.Severity, logger *logr.Logger, filter LogFilter, file string, line int, alsoToStderr bool, args ...interface{}) { +func (l *loggingT) printWithFileLine(s severity.Severity, logger *logWriter, 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 { + // If a logger is set and doesn't support writing a formatted buffer, + // we clear the generated header as we rely on the backing + // logger implementation to print headers. + if logger != nil && logger.writeKlogBuffer == nil { buffer.PutBuffer(buf) buf = buffer.GetBuffer() } @@ -753,7 +760,7 @@ func (l *loggingT) printWithFileLine(s severity.Severity, logger *logr.Logger, f } // if loggr is specified, will call loggr.Error, otherwise output with logging module. -func (l *loggingT) errorS(err error, logger *logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) { +func (l *loggingT) errorS(err error, logger *logWriter, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) { if filter != nil { msg, keysAndValues = filter.FilterS(msg, keysAndValues) } @@ -765,7 +772,7 @@ func (l *loggingT) errorS(err error, logger *logr.Logger, filter LogFilter, dept } // if loggr is specified, will call loggr.Info, otherwise output with logging module. -func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) { +func (l *loggingT) infoS(logger *logWriter, filter LogFilter, depth int, msg string, keysAndValues ...interface{}) { if filter != nil { msg, keysAndValues = filter.FilterS(msg, keysAndValues) } @@ -846,7 +853,7 @@ func LogToStderr(stderr bool) { } // output writes the data to the log files and releases the buffer. -func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buffer, depth int, file string, line int, alsoToStderr bool) { +func (l *loggingT) output(s severity.Severity, logger *logWriter, buf *buffer.Buffer, depth int, file string, line int, alsoToStderr bool) { var isLocked = true l.mu.Lock() defer func() { @@ -862,13 +869,17 @@ func (l *loggingT) output(s severity.Severity, log *logr.Logger, buf *buffer.Buf } } data := buf.Bytes() - if log != nil { - // TODO: set 'severity' and caller information as structured log info - // keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line} - if s == severity.ErrorLog { - logging.logger.WithCallDepth(depth+3).Error(nil, string(data)) + if logger != nil { + if logger.writeKlogBuffer != nil { + logger.writeKlogBuffer(data) } else { - log.WithCallDepth(depth + 3).Info(string(data)) + // TODO: set 'severity' and caller information as structured log info + // keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line} + if s == severity.ErrorLog { + logger.WithCallDepth(depth+3).Error(nil, string(data)) + } else { + logger.WithCallDepth(depth + 3).Info(string(data)) + } } } else if l.toStderr { os.Stderr.Write(data) @@ -1277,7 +1288,7 @@ func (l *loggingT) setV(pc uintptr) Level { // See the documentation of V for more information. type Verbose struct { enabled bool - logr *logr.Logger + logger *logWriter } func newVerbose(level Level, b bool) Verbose { @@ -1285,7 +1296,7 @@ func newVerbose(level Level, b bool) Verbose { return Verbose{b, nil} } v := logging.logger.V(int(level)) - return Verbose{b, &v} + return Verbose{b, &logWriter{Logger: v, writeKlogBuffer: logging.loggerOptions.writeKlogBuffer}} } // V reports whether verbosity at the call site is at least the requested level. @@ -1359,7 +1370,7 @@ func (v Verbose) Enabled() bool { // See the documentation of V for usage. func (v Verbose) Info(args ...interface{}) { if v.enabled { - logging.print(severity.InfoLog, v.logr, logging.filter, args...) + logging.print(severity.InfoLog, v.logger, logging.filter, args...) } } @@ -1367,7 +1378,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(severity.InfoLog, v.logr, logging.filter, depth, args...) + logging.printDepth(severity.InfoLog, v.logger, logging.filter, depth, args...) } } @@ -1375,7 +1386,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(severity.InfoLog, v.logr, logging.filter, args...) + logging.println(severity.InfoLog, v.logger, logging.filter, args...) } } @@ -1383,7 +1394,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(severity.InfoLog, v.logr, logging.filter, depth, args...) + logging.printlnDepth(severity.InfoLog, v.logger, logging.filter, depth, args...) } } @@ -1391,7 +1402,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(severity.InfoLog, v.logr, logging.filter, format, args...) + logging.printf(severity.InfoLog, v.logger, logging.filter, format, args...) } } @@ -1399,7 +1410,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(severity.InfoLog, v.logr, logging.filter, depth, format, args...) + logging.printfDepth(severity.InfoLog, v.logger, logging.filter, depth, format, args...) } } @@ -1407,7 +1418,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, logging.filter, 0, msg, keysAndValues...) + logging.infoS(v.logger, logging.filter, 0, msg, keysAndValues...) } } @@ -1421,14 +1432,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, logging.filter, depth, msg, keysAndValues...) + logging.infoS(v.logger, 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, logging.filter, 0, msg, args...) + logging.errorS(err, v.logger, logging.filter, 0, msg, args...) } } @@ -1436,7 +1447,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, logging.filter, 0, msg, keysAndValues...) + logging.errorS(err, v.logger, logging.filter, 0, msg, keysAndValues...) } } diff --git a/klog_test.go b/klog_test.go index 96f337ac5..58eeb2671 100644 --- a/klog_test.go +++ b/klog_test.go @@ -2196,7 +2196,7 @@ func TestSettingsDeepCopy(t *testing.T) { logger := logr.Discard() settings := settings{ - logger: &logger, + logger: &logWriter{Logger: logger}, vmodule: moduleSpec{ filter: []modulePat{ {pattern: "a"}, From 7a9f099eabbb6596b81edec284ec38802718ba5f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 24 Jan 2023 11:04:25 +0100 Subject: [PATCH 119/125] textlogger: write unstructured klog output directly Textlogger can (but doesn't have to) be used to write klog's unstructured output from calls like Infof directly to the output stream. This has two advantages: - traditional output remains exactly the same as before, which is particularly important for multi-line messages because those get quoted otherwise - performance is better because re-encoding the output string is avoided --- test/output.go | 20 +++++++++++++++++--- textlogger/textlogger.go | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/test/output.go b/test/output.go index 4dc52f766..ecba5d447 100644 --- a/test/output.go +++ b/test/output.go @@ -39,6 +39,7 @@ import ( "github.com/go-logr/logr" "k8s.io/klog/v2" + "k8s.io/klog/v2/textlogger" ) // InitKlog must be called in a test to configure klog for testing. @@ -577,7 +578,7 @@ func Output(t *testing.T, config OutputConfig) { if config.AsBackend { testOutput(t, printWithKlogLine-1, func(buffer *bytes.Buffer) { - klog.SetLogger(config.NewLogger(buffer, 10, test.vmodule)) + setLogger(config.NewLogger(buffer, 10, test.vmodule)) printWithKlog(test) }) return @@ -765,10 +766,11 @@ func Output(t *testing.T, config OutputConfig) { for i, test := range tests { t.Run(test.name, func(t *testing.T) { var buffer bytes.Buffer + haveWriteKlogBuffer := false if config.NewLogger == nil { klog.SetOutput(&buffer) } else { - klog.SetLogger(config.NewLogger(&buffer, 10, "")) + haveWriteKlogBuffer = setLogger(config.NewLogger(&buffer, 10, "")) defer klog.ClearLogger() } test.logFunc() @@ -789,6 +791,7 @@ func Output(t *testing.T, config OutputConfig) { // result, including a trailing newline, to // Logger.Info. if config.NewLogger != nil && + !haveWriteKlogBuffer && !strings.HasSuffix(test.name, "S") && !strings.HasSuffix(test.name, "SDepth") { // klog: I output.go:] hello world @@ -851,7 +854,7 @@ func Benchmark(b *testing.B, config OutputConfig) { } if config.AsBackend { - klog.SetLogger(config.NewLogger(io.Discard, 10, "")) + setLogger(config.NewLogger(io.Discard, 10, "")) for i := 0; i < b.N; i++ { printWithKlog(test) } @@ -871,6 +874,17 @@ func Benchmark(b *testing.B, config OutputConfig) { } } +func setLogger(logger logr.Logger) bool { + haveWriteKlogBuffer := false + var opts []klog.LoggerOption + if writer, ok := logger.GetSink().(textlogger.KlogBufferWriter); ok { + opts = append(opts, klog.WriteKlogBuffer(writer.WriteKlogBuffer)) + haveWriteKlogBuffer = true + } + klog.SetLoggerWithOptions(logger, opts...) + return haveWriteKlogBuffer +} + func copySlice(in []interface{}) []interface{} { return append([]interface{}{}, in...) } diff --git a/textlogger/textlogger.go b/textlogger/textlogger.go index be946e0f5..b53b3b796 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -134,6 +134,10 @@ func (l *tlogger) print(err error, s severity.Severity, msg string, kvList []int l.config.co.output.Write(b.Bytes()) } +func (l *tlogger) WriteKlogBuffer(data []byte) { + l.config.co.output.Write(data) +} + // 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. @@ -152,5 +156,15 @@ func (l *tlogger) WithValues(kvList ...interface{}) logr.LogSink { return &new } +// KlogBufferWriter is implemented by the textlogger LogSink. +type KlogBufferWriter interface { + // WriteKlogBuffer takes a pre-formatted buffer prepared by klog and + // writes it unchanged to the output stream. Can be used with + // klog.WriteKlogBuffer when setting a logger through + // klog.SetLoggerWithOptions. + WriteKlogBuffer([]byte) +} + var _ logr.LogSink = &tlogger{} var _ logr.CallDepthLogSink = &tlogger{} +var _ KlogBufferWriter = &tlogger{} From 8b4cfd2c73a1eb6dcd9c36b199f9f1db026f078a Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 1 Feb 2023 17:59:21 +0100 Subject: [PATCH 120/125] ktesting: use klog-like header In practice, replacing normal klog text output with ktesting lost the time stamps. This is perhaps not important for unit tests, but for benchmarking those can be crucial. Now ktesting uses a stripped down klog header: - The source code location comes first, because that is printed by the testing package. - Instead if INFO and ERROR, the short I and E are used. - The useless tid/pid part isn't present. Example: $ go test -v ./ktesting/example/ === RUN TestKlogr example_test.go:45: I0201 17:58:36.235936] hello world example_test.go:46: E0201 17:58:36.236052] failed err="failed: some error" example_test.go:47: I0201 17:58:36.236086] verbosity 1 example_test.go:48: I0201 17:58:36.236110] main/helper: with prefix example_test.go:50: I0201 17:58:36.236154] key/value pairs int=1 float=2 pair="(1, 2)" raw={Name:joe NS:kube-system} kobj="kube-system/joe" example_test.go:57: I0201 17:58:36.236187] info message level 4 example_test.go:58: I0201 17:58:36.236209] info message level 5 --- PASS: TestKlogr (0.00s) PASS --- internal/buffer/buffer.go | 30 ++++++++++++++++++++++++++- ktesting/testinglogger.go | 37 +++++++++++++++++++--------------- ktesting/testinglogger_test.go | 25 ++++++++++++----------- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index d53b49da3..121cbf70b 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -99,7 +99,8 @@ func (buf *Buffer) someDigits(i, d int) int { return copy(buf.Tmp[i:], buf.Tmp[j:]) } -// FormatHeader formats a log header using the provided file name and line number. +// FormatHeader formats a log header using the provided file name and line number +// and writes it into the buffer. 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 @@ -135,3 +136,30 @@ func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now buf.Tmp[n+2] = ' ' buf.Write(buf.Tmp[:n+3]) } + +// SprintHeader formats a log header and returns a string. This is a simpler +// version of FormatHeader for use in ktesting. +func (buf *Buffer) SprintHeader(s severity.Severity, now time.Time) string { + 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] = ']' + return string(buf.Tmp[:22]) +} diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 0c87a953b..cbdd0f647 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -42,7 +42,6 @@ limitations under the License. package ktesting import ( - "bytes" "strings" "sync" "time" @@ -50,8 +49,10 @@ import ( "github.com/go-logr/logr" "k8s.io/klog/v2" + "k8s.io/klog/v2/internal/buffer" "k8s.io/klog/v2/internal/dbg" "k8s.io/klog/v2/internal/serialize" + "k8s.io/klog/v2/internal/severity" "k8s.io/klog/v2/internal/verbosity" ) @@ -230,19 +231,19 @@ type Underlier interface { GetBuffer() Buffer } -type buffer struct { +type logBuffer struct { mutex sync.Mutex text strings.Builder log Log } -func (b *buffer) String() string { +func (b *logBuffer) String() string { b.mutex.Lock() defer b.mutex.Unlock() return b.text.String() } -func (b *buffer) Data() Log { +func (b *logBuffer) Data() Log { b.mutex.Lock() defer b.mutex.Unlock() return b.log.DeepCopy() @@ -263,7 +264,7 @@ type tloggerShared struct { testName string config *Config - buffer buffer + buffer logBuffer callDepth int } @@ -318,9 +319,9 @@ func (l tlogger) Info(level int, msg string, kvList ...interface{}) { } l.shared.t.Helper() - buffer := &bytes.Buffer{} - serialize.MergeAndFormatKVs(buffer, l.values, kvList) - l.log(LogInfo, msg, level, buffer, nil, kvList) + buf := buffer.GetBuffer() + serialize.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) + l.log(LogInfo, msg, level, buf, nil, kvList) } func (l tlogger) Enabled(level int) bool { @@ -336,24 +337,28 @@ func (l tlogger) Error(err error, msg string, kvList ...interface{}) { } l.shared.t.Helper() - buffer := &bytes.Buffer{} + buf := buffer.GetBuffer() if err != nil { - serialize.KVFormat(buffer, "err", err) + serialize.KVFormat(&buf.Buffer, "err", err) } - serialize.MergeAndFormatKVs(buffer, l.values, kvList) - l.log(LogError, msg, 0, buffer, err, kvList) + serialize.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) + l.log(LogError, msg, 0, buf, err, kvList) } -func (l tlogger) log(what LogType, msg string, level int, buffer *bytes.Buffer, err error, kvList []interface{}) { +func (l tlogger) log(what LogType, msg string, level int, buf *buffer.Buffer, err error, kvList []interface{}) { l.shared.t.Helper() - args := []interface{}{what} + s := severity.InfoLog + if what == LogError { + s = severity.ErrorLog + } + args := []interface{}{buf.SprintHeader(s, time.Now())} if l.prefix != "" { args = append(args, l.prefix+":") } args = append(args, msg) - if buffer.Len() > 0 { + if buf.Len() > 0 { // Skip leading space inserted by serialize.KVListFormat. - args = append(args, string(buffer.Bytes()[1:])) + args = append(args, string(buf.Bytes()[1:])) } l.shared.t.Log(args...) diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index bb1efe6a1..2609bce12 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -33,78 +33,78 @@ func TestInfo(t *testing.T) { "should log with values passed to keysAndValues": { text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `INFO test akey="avalue" + expectedOutput: `Ixxx test akey="avalue" `, }, "should support single name": { names: []string{"hello"}, text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `INFO hello: test akey="avalue" + expectedOutput: `Ixxx hello: test akey="avalue" `, }, "should support multiple names": { names: []string{"hello", "world"}, text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `INFO hello/world: test akey="avalue" + expectedOutput: `Ixxx hello/world: test akey="avalue" `, }, "should not print duplicate keys with the same value": { text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"}, - expectedOutput: `INFO test akey="avalue" akey="avalue" + expectedOutput: `Ixxx test akey="avalue" akey="avalue" `, }, "should only print the last duplicate key when the values are passed to Info": { text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"}, - expectedOutput: `INFO test akey="avalue" akey="avalue2" + expectedOutput: `Ixxx test akey="avalue" akey="avalue2" `, }, "should only print the duplicate key that is passed to Info if one was passed to the logger": { withValues: []interface{}{"akey", "avalue"}, text: "test", keysAndValues: []interface{}{"akey", "avalue"}, - expectedOutput: `INFO test akey="avalue" + expectedOutput: `Ixxx test akey="avalue" `, }, "should only print the key passed to Info when one is already set on the logger": { withValues: []interface{}{"akey", "avalue"}, text: "test", keysAndValues: []interface{}{"akey", "avalue2"}, - expectedOutput: `INFO test akey="avalue2" + expectedOutput: `Ixxx test akey="avalue2" `, }, "should correctly handle odd-numbers of KVs": { text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: `INFO test akey="avalue" akey2="(MISSING)" + expectedOutput: `Ixxx test akey="avalue" akey2="(MISSING)" `, }, "should correctly html characters": { text: "test", keysAndValues: []interface{}{"akey", "<&>"}, - expectedOutput: `INFO test akey="<&>" + expectedOutput: `Ixxx test akey="<&>" `, }, "should correctly handle odd-numbers of KVs in both log values and Info args": { withValues: []interface{}{"basekey1", "basevar1", "basekey2"}, text: "test", keysAndValues: []interface{}{"akey", "avalue", "akey2"}, - expectedOutput: `INFO test basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" + expectedOutput: `Ixxx test basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)" `, }, "should correctly print regular error types": { text: "test", keysAndValues: []interface{}{"err", errors.New("whoops")}, - expectedOutput: `INFO test err="whoops" + expectedOutput: `Ixxx test err="whoops" `, }, "should correctly print regular error types when using logr.Error": { text: "test", err: errors.New("whoops"), - expectedOutput: `ERROR test err="whoops" + expectedOutput: `Exxx test err="whoops" `, }, } @@ -124,6 +124,7 @@ func TestInfo(t *testing.T) { } actual := buffer.String() + actual = regexp.MustCompile(`([IE])[[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}\] `).ReplaceAllString(actual, `${1}xxx `) if actual != test.expectedOutput { t.Errorf("Expected:\n%sActual:\n%s\n", test.expectedOutput, actual) } From d1139259a646881a7e20d0c2ffb3fe0a23170e89 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 1 Feb 2023 19:25:35 +0100 Subject: [PATCH 121/125] ktesting: capture log entries only if requested Most users won't need this feature. It was enabled by default to keep the API simple and because the primary goal was unit testing, but benchmarks also need this and there unnecessary overhead needs to be avoided. --- ktesting/contextual_test.go | 13 ++++++++++--- ktesting/example_test.go | 28 +++++++++++++++++++++++++++- ktesting/options.go | 16 ++++++++++++++++ ktesting/testinglogger.go | 22 ++++++++++++++++++++++ ktesting/testinglogger_test.go | 4 +++- 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/ktesting/contextual_test.go b/ktesting/contextual_test.go index f260fb0aa..1b927e8ef 100644 --- a/ktesting/contextual_test.go +++ b/ktesting/contextual_test.go @@ -16,7 +16,8 @@ import ( ) func TestContextual(t *testing.T) { - logger, ctx := ktesting.NewTestContext(t) + var buffer ktesting.BufferTL + logger, ctx := ktesting.NewTestContext(&buffer) doSomething(ctx) @@ -33,8 +34,14 @@ func TestContextual(t *testing.T) { } actual := testingLogger.GetBuffer().String() - expected := `INFO hello world -INFO foo: hello also from me + if actual != "" { + t.Errorf("testinglogger should not have buffered, got:\n%s", actual) + } + + actual = buffer.String() + actual = headerRe.ReplaceAllString(actual, "${1}xxx] ") + expected := `Ixxx] hello world +Ixxx] foo: hello also from me ` if actual != expected { t.Errorf("mismatch in captured output, expected:\n%s\ngot:\n%s\n", expected, actual) diff --git a/ktesting/example_test.go b/ktesting/example_test.go index c2fa390c5..81f0a46ed 100644 --- a/ktesting/example_test.go +++ b/ktesting/example_test.go @@ -25,7 +25,12 @@ import ( ) func ExampleUnderlier() { - logger := ktesting.NewLogger(ktesting.NopTL{}, ktesting.NewConfig(ktesting.Verbosity(4))) + logger := ktesting.NewLogger(ktesting.NopTL{}, + ktesting.NewConfig( + ktesting.Verbosity(4), + ktesting.BufferLogs(true), + ), + ) logger.Error(errors.New("failure"), "I failed", "what", "something") logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world") @@ -69,3 +74,24 @@ func ExampleUnderlier() { // log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err: WithKVList:[] ParameterKVList:[]} // log entry #4: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:higher verbosity Verbosity:4 Err: WithKVList:[] ParameterKVList:[]} } + +func ExampleDefaults() { + var buffer ktesting.BufferTL + logger := ktesting.NewLogger(&buffer, ktesting.NewConfig()) + + logger.Error(errors.New("failure"), "I failed", "what", "something") + logger.V(5).Info("Logged at level 5.") + logger.V(6).Info("Not logged at level 6.") + + testingLogger, ok := logger.GetSink().(ktesting.Underlier) + if !ok { + panic("Should have had a ktesting LogSink!?") + } + fmt.Printf(">> %s <<\n", testingLogger.GetBuffer().String()) // Should be empty. + fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) // Should not be empty. + + // Output: + // >> << + // E...] I failed err="failure" what="something" + // I...] Logged at level 5. +} diff --git a/ktesting/options.go b/ktesting/options.go index 743b51eba..ab78fe690 100644 --- a/ktesting/options.go +++ b/ktesting/options.go @@ -50,6 +50,7 @@ type configOptions struct { verbosityFlagName string vmoduleFlagName string verbosityDefault int + bufferLogs bool } // VerbosityFlagName overrides the default -testing.v for the verbosity level. @@ -94,6 +95,21 @@ func Verbosity(level int) ConfigOption { } } +// BufferLogs controls whether log entries are captured in memory in addition +// to being printed. Off by default. Unit tests that want to verify that +// log entries are emitted as expected can turn this on and then retrieve +// the captured log through the Underlier LogSink interface. +// +// # Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func BufferLogs(enabled bool) ConfigOption { + return func(co *configOptions) { + co.bufferLogs = enabled + } +} + // NewConfig returns a configuration with recommended defaults and optional // modifications. Command line flags are not bound to any FlagSet yet. // diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index cbdd0f647..506485e3a 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -42,6 +42,7 @@ limitations under the License. package ktesting import ( + "fmt" "strings" "sync" "time" @@ -81,6 +82,23 @@ func (n NopTL) Log(args ...interface{}) {} var _ TL = NopTL{} +// BufferTL implements TL with an in-memory buffer. +// +// # Experimental +// +// Notice: This type is EXPERIMENTAL and may be changed or removed in a +// later release. +type BufferTL struct { + strings.Builder +} + +func (n *BufferTL) Helper() {} +func (n *BufferTL) Log(args ...interface{}) { + n.Builder.WriteString(fmt.Sprintln(args...)) +} + +var _ TL = &BufferTL{} + // NewLogger constructs a new logger for the given test interface. // // Beware that testing.T does not support logging after the test that @@ -362,6 +380,10 @@ func (l tlogger) log(what LogType, msg string, level int, buf *buffer.Buffer, er } l.shared.t.Log(args...) + if !l.shared.config.co.bufferLogs { + return + } + l.shared.buffer.mutex.Lock() defer l.shared.buffer.mutex.Unlock() diff --git a/ktesting/testinglogger_test.go b/ktesting/testinglogger_test.go index 2609bce12..6de1834d4 100644 --- a/ktesting/testinglogger_test.go +++ b/ktesting/testinglogger_test.go @@ -21,6 +21,8 @@ import ( "k8s.io/klog/v2/ktesting" ) +var headerRe = regexp.MustCompile(`([IE])[[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}\] `) + func TestInfo(t *testing.T) { tests := map[string]struct { text string @@ -124,7 +126,7 @@ func TestInfo(t *testing.T) { } actual := buffer.String() - actual = regexp.MustCompile(`([IE])[[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}\] `).ReplaceAllString(actual, `${1}xxx `) + actual = headerRe.ReplaceAllString(actual, `${1}xxx `) if actual != test.expectedOutput { t.Errorf("Expected:\n%sActual:\n%s\n", test.expectedOutput, actual) } From 258295676b4b6a3fa3fbf9cd065dd0c58e2ada3f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 2 Feb 2023 19:56:02 +0100 Subject: [PATCH 122/125] buffer: restore dropping of too large buffers This was removed when introducing sync.Pool in commit 1a1367cc64e28f8bf3dcc04cd74010cc67bdd57d because it seemed unnecessary, but an open issue about the sync.Pool documentation shows that a size limit may be useful after all. --- internal/buffer/buffer.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index d53b49da3..fc9df1d70 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -55,6 +55,17 @@ func GetBuffer() *Buffer { // PutBuffer returns a buffer to the free list. func PutBuffer(b *Buffer) { + if b.Len() >= 256 { + // Let big buffers die a natural death, without relying on + // sync.Pool behavior. The documentation implies that items may + // get deallocated while stored there ("If the Pool holds the + // only reference when this [= be removed automatically] + // happens, the item might be deallocated."), but + // https://github.com/golang/go/issues/23199 leans more towards + // having such a size limit. + return + } + buffers.Put(b) } From 1b27ee81c79d836aa1aa5369f4f7a7bc832fd21b Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 2 Feb 2023 13:23:15 +0100 Subject: [PATCH 123/125] ktesting: allow overriding default formatter The intended usage is to replace fmt.Sprintf("%+v") with gomega.format.Object + YAML support, therefore the only public API change is in the (still experimental) ktesting. Internally the additional function pointer gets passed through via a new Formatter struct. To minimize the impact on klog and textlogger, the package-level functions still exist and use an empty Formatter. --- internal/serialize/keyvalues.go | 46 ++++++++++++++++++++++++++------- ktesting/example_test.go | 19 ++++++++------ ktesting/options.go | 16 ++++++++++++ ktesting/testinglogger.go | 10 ++++--- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index f9558c3d2..1dc81a15f 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -95,9 +95,15 @@ func MergeKVs(first, second []interface{}) []interface{} { return merged } +type Formatter struct { + AnyToStringHook AnyToStringFunc +} + +type AnyToStringFunc func(v interface{}) string + // MergeKVsInto is a variant of MergeKVs which directly formats the key/value // pairs into a buffer. -func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { +func (f Formatter) MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { if len(first) == 0 && len(second) == 0 { // Nothing to do at all. return @@ -107,7 +113,7 @@ func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { // Nothing to be overridden, second slice is well-formed // and can be used directly. for i := 0; i < len(second); i += 2 { - KVFormat(b, second[i], second[i+1]) + f.KVFormat(b, second[i], second[i+1]) } return } @@ -127,24 +133,28 @@ func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { if overrides[key] { continue } - KVFormat(b, key, first[i+1]) + f.KVFormat(b, key, first[i+1]) } // Round down. l := len(second) l = l / 2 * 2 for i := 1; i < l; i += 2 { - KVFormat(b, second[i-1], second[i]) + f.KVFormat(b, second[i-1], second[i]) } if len(second)%2 == 1 { - KVFormat(b, second[len(second)-1], missingValue) + f.KVFormat(b, second[len(second)-1], missingValue) } } +func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) { + Formatter{}.MergeAndFormatKVs(b, first, second) +} + 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{}) { +func (f Formatter) KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { for i := 0; i < len(keysAndValues); i += 2 { var v interface{} k := keysAndValues[i] @@ -153,13 +163,17 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { } else { v = missingValue } - KVFormat(b, k, v) + f.KVFormat(b, k, v) } } +func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) { + Formatter{}.KVListFormat(b, keysAndValues...) +} + // KVFormat serializes one key/value pair into the provided buffer. // A space gets inserted before the pair. -func KVFormat(b *bytes.Buffer, k, v interface{}) { +func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) { 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 @@ -203,7 +217,7 @@ func KVFormat(b *bytes.Buffer, k, v interface{}) { case string: writeStringValue(b, true, value) default: - writeStringValue(b, false, fmt.Sprintf("%+v", value)) + writeStringValue(b, false, f.AnyToString(value)) } case []byte: // In https://github.com/kubernetes/klog/pull/237 it was decided @@ -220,8 +234,20 @@ func KVFormat(b *bytes.Buffer, k, v interface{}) { b.WriteByte('=') b.WriteString(fmt.Sprintf("%+q", v)) default: - writeStringValue(b, false, fmt.Sprintf("%+v", v)) + writeStringValue(b, false, f.AnyToString(v)) + } +} + +func KVFormat(b *bytes.Buffer, k, v interface{}) { + Formatter{}.KVFormat(b, k, v) +} + +// AnyToString is the historic fallback formatter. +func (f Formatter) AnyToString(v interface{}) string { + if f.AnyToStringHook != nil { + return f.AnyToStringHook(v) } + return fmt.Sprintf("%+v", v) } // StringerToString converts a Stringer to a string, diff --git a/ktesting/example_test.go b/ktesting/example_test.go index 81f0a46ed..3fc688bf1 100644 --- a/ktesting/example_test.go +++ b/ktesting/example_test.go @@ -29,10 +29,13 @@ func ExampleUnderlier() { ktesting.NewConfig( ktesting.Verbosity(4), ktesting.BufferLogs(true), + ktesting.AnyToString(func(value interface{}) string { + return fmt.Sprintf("### %+v ###", value) + }), ), ) - logger.Error(errors.New("failure"), "I failed", "what", "something") + logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ field int }{field: 1}) logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world") logger.WithValues("request", 42, "anotherValue", "fish").Info("hello world 2", "yetAnotherValue", "thanks") logger.WithName("example").Info("with name") @@ -62,24 +65,24 @@ func ExampleUnderlier() { } // Output: - // ERROR I failed err="failure" what="something" - // INFO hello world request=42 anotherValue="fish" - // INFO hello world 2 request=42 anotherValue="fish" yetAnotherValue="thanks" + // ERROR I failed err="failure" what="something" data=### {field:1} ### + // INFO hello world request=### 42 ### anotherValue="fish" + // INFO hello world 2 request=### 42 ### anotherValue="fish" yetAnotherValue="thanks" // INFO example: with name // INFO higher verbosity // - // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something]} + // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something data {field:1}]} // log entry #1: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[]} // log entry #2: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world 2 Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[yetAnotherValue thanks]} // log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err: WithKVList:[] ParameterKVList:[]} // log entry #4: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:higher verbosity Verbosity:4 Err: WithKVList:[] ParameterKVList:[]} } -func ExampleDefaults() { +func ExampleNewLogger() { var buffer ktesting.BufferTL logger := ktesting.NewLogger(&buffer, ktesting.NewConfig()) - logger.Error(errors.New("failure"), "I failed", "what", "something") + logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ field int }{field: 1}) logger.V(5).Info("Logged at level 5.") logger.V(6).Info("Not logged at level 6.") @@ -92,6 +95,6 @@ func ExampleDefaults() { // Output: // >> << - // E...] I failed err="failure" what="something" + // E...] I failed err="failure" what="something" data={field:1} // I...] Logged at level 5. } diff --git a/ktesting/options.go b/ktesting/options.go index ab78fe690..c9bb3da80 100644 --- a/ktesting/options.go +++ b/ktesting/options.go @@ -20,6 +20,7 @@ import ( "flag" "strconv" + "k8s.io/klog/v2/internal/serialize" "k8s.io/klog/v2/internal/verbosity" ) @@ -47,12 +48,27 @@ type Config struct { type ConfigOption func(co *configOptions) type configOptions struct { + anyToString serialize.AnyToStringFunc verbosityFlagName string vmoduleFlagName string verbosityDefault int bufferLogs bool } +// AnyToString overrides the default formatter for values that are not +// supported directly by klog. The default is `fmt.Sprintf("%+v")`. +// The formatter must not panic. +// +// # Experimental +// +// Notice: This function is EXPERIMENTAL and may be changed or removed in a +// later release. +func AnyToString(anyToString func(value interface{}) string) ConfigOption { + return func(co *configOptions) { + co.anyToString = anyToString + } +} + // VerbosityFlagName overrides the default -testing.v for the verbosity level. // // # Experimental diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 506485e3a..434b1533c 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -118,6 +118,9 @@ func NewLogger(t TL, c *Config) logr.Logger { config: c, }, } + if c.co.anyToString != nil { + l.shared.formatter.AnyToStringHook = c.co.anyToString + } type testCleanup interface { Cleanup(func()) @@ -280,6 +283,7 @@ type tloggerShared struct { // it logs after test completion. goroutineWarningDone bool + formatter serialize.Formatter testName string config *Config buffer logBuffer @@ -338,7 +342,7 @@ func (l tlogger) Info(level int, msg string, kvList ...interface{}) { l.shared.t.Helper() buf := buffer.GetBuffer() - serialize.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) + l.shared.formatter.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) l.log(LogInfo, msg, level, buf, nil, kvList) } @@ -357,9 +361,9 @@ func (l tlogger) Error(err error, msg string, kvList ...interface{}) { l.shared.t.Helper() buf := buffer.GetBuffer() if err != nil { - serialize.KVFormat(&buf.Buffer, "err", err) + l.shared.formatter.KVFormat(&buf.Buffer, "err", err) } - serialize.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) + l.shared.formatter.MergeAndFormatKVs(&buf.Buffer, l.values, kvList) l.log(LogError, msg, 0, buf, err, kvList) } From e092d8942780f81a2e4a255467b1a8bfcc111eef Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 8 Feb 2023 14:48:16 +0100 Subject: [PATCH 124/125] ktesting: support verbosity changes at runtime Being able to change the verbosity at runtime is useful. It is already supported by the underlying code, ktesting and its Config struct just didn't expose it. --- ktesting/example_test.go | 23 +++++++++++++++++++++++ ktesting/options.go | 14 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ktesting/example_test.go b/ktesting/example_test.go index 3fc688bf1..9d887932e 100644 --- a/ktesting/example_test.go +++ b/ktesting/example_test.go @@ -98,3 +98,26 @@ func ExampleNewLogger() { // E...] I failed err="failure" what="something" data={field:1} // I...] Logged at level 5. } + +func ExampleConfig_Verbosity() { + var buffer ktesting.BufferTL + config := ktesting.NewConfig(ktesting.Verbosity(1)) + logger := ktesting.NewLogger(&buffer, config) + + logger.Info("initial verbosity", "v", config.Verbosity().String()) + logger.V(2).Info("now you don't see me") + if err := config.Verbosity().Set("2"); err != nil { + logger.Error(err, "setting verbosity to 2") + } + logger.V(2).Info("now you see me") + if err := config.Verbosity().Set("1"); err != nil { + logger.Error(err, "setting verbosity to 1") + } + logger.V(2).Info("now I'm gone again") + + fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) + + // Output: + // I...] initial verbosity v="1" + // I...] now you see me +} diff --git a/ktesting/options.go b/ktesting/options.go index c9bb3da80..d039c40b0 100644 --- a/ktesting/options.go +++ b/ktesting/options.go @@ -39,6 +39,20 @@ type Config struct { co configOptions } +// Verbosity returns a value instance that can be used to query (via String) or +// modify (via Set) the verbosity threshold. This is thread-safe and can be +// done at runtime. +func (c *Config) Verbosity() flag.Value { + return c.vstate.V() +} + +// VModule returns a value instance that can be used to query (via String) or +// modify (via Set) the vmodule settings. This is thread-safe and can be done +// at runtime. +func (c *Config) VModule() flag.Value { + return c.vstate.VModule() +} + // ConfigOption implements functional parameters for NewConfig. // // # Experimental From a0fea0c4ae59301dd74006e113fee053aeff55bc Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 8 Feb 2023 15:00:20 +0100 Subject: [PATCH 125/125] textlogger: verbosity changes through public API By embedding *verbosity.VState in Config, users of the package already had access to V and VModule, but that had two drawbacks: - not easy to discover - unclean separate between internal and external API Providing explicit functions is better. --- ktesting/testinglogger.go | 3 +++ textlogger/example_test.go | 50 ++++++++++++++++++++++++++++++++++++++ textlogger/options.go | 24 ++++++++++++++---- textlogger/textlogger.go | 5 +++- 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 textlogger/example_test.go diff --git a/ktesting/testinglogger.go b/ktesting/testinglogger.go index 434b1533c..14a22bf40 100644 --- a/ktesting/testinglogger.go +++ b/ktesting/testinglogger.go @@ -107,6 +107,9 @@ var _ TL = &BufferTL{} // that output will be printed via the global klog logger with // ` leaked goroutine` as prefix. // +// Verbosity can be modified at any time through the Config.V and +// Config.VModule API. +// // # Experimental // // Notice: This type is EXPERIMENTAL and may be changed or removed in a diff --git a/textlogger/example_test.go b/textlogger/example_test.go new file mode 100644 index 000000000..9038d2bdd --- /dev/null +++ b/textlogger/example_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 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_test + +import ( + "bytes" + "fmt" + "regexp" + + "k8s.io/klog/v2/textlogger" +) + +var headerRe = regexp.MustCompile(`([IE])[[:digit:]]{4} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{6}[[:space:]]+[[:digit:]]+ example_test.go:[[:digit:]]+\] `) + +func ExampleConfig_Verbosity() { + var buffer bytes.Buffer + config := textlogger.NewConfig(textlogger.Verbosity(1), textlogger.Output(&buffer)) + logger := textlogger.NewLogger(config) + + logger.Info("initial verbosity", "v", config.Verbosity().String()) + logger.V(2).Info("now you don't see me") + if err := config.Verbosity().Set("2"); err != nil { + logger.Error(err, "setting verbosity to 2") + } + logger.V(2).Info("now you see me") + if err := config.Verbosity().Set("1"); err != nil { + logger.Error(err, "setting verbosity to 1") + } + logger.V(2).Info("now I'm gone again") + + fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) + + // Output: + // I...] "initial verbosity" v="1" + // I...] "now you see me" +} diff --git a/textlogger/options.go b/textlogger/options.go index 5803ccecb..db4cbebdc 100644 --- a/textlogger/options.go +++ b/textlogger/options.go @@ -36,8 +36,22 @@ import ( // Notice: This type is EXPERIMENTAL and may be changed or removed in a // later release. type Config struct { - *verbosity.VState - co configOptions + vstate *verbosity.VState + co configOptions +} + +// Verbosity returns a value instance that can be used to query (via String) or +// modify (via Set) the verbosity threshold. This is thread-safe and can be +// done at runtime. +func (c *Config) Verbosity() flag.Value { + return c.vstate.V() +} + +// VModule returns a value instance that can be used to query (via String) or +// modify (via Set) the vmodule settings. This is thread-safe and can be done +// at runtime. +func (c *Config) VModule() flag.Value { + return c.vstate.VModule() } // ConfigOption implements functional parameters for NewConfig. @@ -111,7 +125,7 @@ func Output(output io.Writer) ConfigOption { // later release. func NewConfig(opts ...ConfigOption) *Config { c := &Config{ - VState: verbosity.New(), + vstate: verbosity.New(), co: configOptions{ verbosityFlagName: "v", vmoduleFlagName: "vmodule", @@ -123,7 +137,7 @@ func NewConfig(opts ...ConfigOption) *Config { opt(&c.co) } - c.V().Set(strconv.FormatInt(int64(c.co.verbosityDefault), 10)) + c.Verbosity().Set(strconv.FormatInt(int64(c.co.verbosityDefault), 10)) return c } @@ -134,6 +148,6 @@ func NewConfig(opts ...ConfigOption) *Config { // 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.Verbosity(), 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 index be946e0f5..87029c2c2 100644 --- a/textlogger/textlogger.go +++ b/textlogger/textlogger.go @@ -50,6 +50,9 @@ var ( // NewLogger constructs a new logger. // +// Verbosity can be modified at any time through the Config.V and +// Config.VModule API. +// // # Experimental // // Notice: This function is EXPERIMENTAL and may be changed or removed in a @@ -82,7 +85,7 @@ func (l *tlogger) WithCallDepth(depth int) logr.LogSink { func (l *tlogger) Enabled(level int) bool { // Skip this function and the Logger.Info call, then // also any additional stack frames from WithCallDepth. - return l.config.Enabled(verbosity.Level(level), 2+l.callDepth) + return l.config.vstate.Enabled(verbosity.Level(level), 2+l.callDepth) } func (l *tlogger) Info(level int, msg string, kvList ...interface{}) {