Repository: kubernetes/klog
Pull request #204 (2021-01-11 22:59:51)

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.


Patch:
diff --git a/OWNERS b/OWNERS
index 380e514..ad5063f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -15,5 +15,5 @@ approvers:
   - tallclair
   - piosz
   - brancz
-  - DirectXMan12
   - lavalamp
+  - serathius
diff --git a/go.mod b/go.mod
index 52c5c24..08a2d0f 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.2.1
+require github.com/go-logr/logr v1.0.0
diff --git a/go.sum b/go.sum
index a685729..a1b90e4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
-github.com/go-logr/logr v0.2.1 h1:fV3MLmabKIZ383XifUjFSwcoGee0v9qgPp8wy5svibE=
-github.com/go-logr/logr v0.2.1/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/hack/tools/go.mod b/hack/tools/go.mod
new file mode 100644
index 0000000..3c94929
--- /dev/null
+++ b/hack/tools/go.mod
@@ -0,0 +1,8 @@
+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
new file mode 100644
index 0000000..e89e59c
--- /dev/null
+++ b/hack/tools/go.sum
@@ -0,0 +1,46 @@
+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
new file mode 100644
index 0000000..008231c
--- /dev/null
+++ b/hack/tools/logcheck/README.md
@@ -0,0 +1,5 @@
+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.
+
+**Installation:**`go install k8s.io/klog/hack/tools/logcheck`
+**Usage:** `$logcheck.go <package_name>`
+`e.g $logcheck ./pkg/kubelet/lifecycle/`
diff --git a/hack/tools/logcheck/main.go b/hack/tools/logcheck/main.go
new file mode 100644
index 0000000..f82f6ea
--- /dev/null
+++ b/hack/tools/logcheck/main.go
@@ -0,0 +1,214 @@
+/*
+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 (
+	"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
+}
+
+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",
+	}
+
+	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
+}
diff --git a/klog.go b/klog.go
index 23cced6..60c88d3 100644
--- a/klog.go
+++ b/klog.go
@@ -81,6 +81,7 @@ import (
 	"math"
 	"os"
 	"path/filepath"
+	"reflect"
 	"runtime"
 	"strconv"
 	"strings"
@@ -283,6 +284,7 @@ func (m *moduleSpec) Get() interface{} {
 
 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
@@ -361,6 +363,7 @@ func (t *traceLocation) Get() interface{} {
 
 var errTraceSyntax = errors.New("syntax error: expect file.go:234")
 
+// Set will sets backtrace value
 // Syntax: -log_backtrace_at=gopherflakes.go:234
 // Note that unlike vmodule the file extension is included here.
 func (t *traceLocation) Set(value string) error {
@@ -433,7 +436,7 @@ func InitFlags(flagset *flag.FlagSet) {
 	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.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.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging")
@@ -506,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
@@ -695,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()
 	}
@@ -707,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, 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()
 	}
@@ -729,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, 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()
 	}
@@ -747,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, 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()
 	}
@@ -768,36 +771,36 @@ 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, 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, 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 {
-		loggr.Error(err, msg, keysAndValues...)
+	if logger != nil {
+		logger.WithCallDepth(depth+2).Error(err, msg, keysAndValues...)
 		return
 	}
-	l.printS(err, msg, keysAndValues...)
+	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, 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 {
-		loggr.Info(msg, keysAndValues...)
+	if logger != nil {
+		logger.WithCallDepth(depth+2).Info(msg, keysAndValues...)
 		return
 	}
-	l.printS(nil, msg, keysAndValues...)
+	l.printS(nil, infoLog, depth+1, msg, keysAndValues...)
 }
 
 // printS is called from infoS and errorS if loggr is not specified.
-// if err arguments is specified, will output to errorLog severity
-func (l *loggingT) printS(err error, msg string, keysAndValues ...interface{}) {
+// set log severity by s
+func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
 	b := &bytes.Buffer{}
 	b.WriteString(fmt.Sprintf("%q", msg))
 	if err != nil {
@@ -805,13 +808,7 @@ func (l *loggingT) printS(err error, msg string, keysAndValues ...interface{}) {
 		b.WriteString(fmt.Sprintf("err=%q", err.Error()))
 	}
 	kvListFormat(b, keysAndValues...)
-	var s severity
-	if err == nil {
-		s = infoLog
-	} else {
-		s = errorLog
-	}
-	l.printDepth(s, logging.logr, nil, 2, b)
+	l.printDepth(s, logging.logr, nil, depth+1, b)
 }
 
 const missingValue = "(MISSING)"
@@ -830,6 +827,8 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
 		switch v.(type) {
 		case string, error:
 			b.WriteString(fmt.Sprintf("%s=%q", k, v))
+		case []byte:
+			b.WriteString(fmt.Sprintf("%s=%+q", k, v))
 		default:
 			if _, ok := v.(fmt.Stringer); ok {
 				b.WriteString(fmt.Sprintf("%s=%q", k, v))
@@ -860,13 +859,26 @@ func (rb *redirectBuffer) Write(bytes []byte) (n int, err error) {
 // 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.
-// All log lines include the 'severity', 'file' and 'line' values attached as
-// structured logging values.
 // 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.logr = logr
+	logging.mu.Lock()
+	defer logging.mu.Unlock()
+
+	logging.logr = &logr
+}
+
+// ClearLogger removes a backing logr implementation if one was set earlier
+// with SetLogger.
+func ClearLogger() {
+	logging.mu.Lock()
+	defer logging.mu.Unlock()
+
+	logging.logr = nil
 }
 
 // SetOutput sets the output destination for all severities
@@ -904,7 +916,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, 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 +928,9 @@ func (l *loggingT) output(s severity, log logr.Logger, buf *buffer, file string,
 		// TODO: set 'severity' and caller information as structured log info
 		// keysAndValues := []interface{}{"severity", severityName[s], "file", file, "line", line}
 		if s == errorLog {
-			l.logr.Error(nil, string(data))
+			l.logr.WithCallDepth(depth+3).Error(nil, string(data))
 		} else {
-			log.Info(string(data))
+			log.WithCallDepth(depth + 3).Info(string(data))
 		}
 	} else if l.toStderr {
 		os.Stderr.Write(data)
@@ -1269,7 +1281,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 +1289,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.
@@ -1315,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)
 	}
@@ -1359,14 +1377,20 @@ func (v Verbose) Infof(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, msg, keysAndValues...)
+		logging.infoS(v.logr, v.filter, 0, msg, keysAndValues...)
 	}
 }
 
+// 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...)
+}
+
 // Deprecated: Use ErrorS instead.
 func (v Verbose) Error(err error, msg string, args ...interface{}) {
 	if v.enabled {
-		logging.errorS(err, v.logr, v.filter, msg, args...)
+		logging.errorS(err, v.logr, v.filter, 0, msg, args...)
 	}
 }
 
@@ -1374,7 +1398,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, msg, keysAndValues...)
+		logging.errorS(err, v.logr, v.filter, 0, msg, keysAndValues...)
 	}
 }
 
@@ -1411,7 +1435,7 @@ func Infof(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, msg, keysAndValues...)
+	logging.infoS(logging.logr, logging.filter, 0, msg, keysAndValues...)
 }
 
 // Warning logs to the WARNING and INFO logs.
@@ -1472,7 +1496,13 @@ func Errorf(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, msg, keysAndValues...)
+	logging.errorS(err, logging.logr, 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...)
 }
 
 // Fatal logs to the FATAL, ERROR, WARNING, and INFO logs,
@@ -1571,6 +1601,13 @@ type KMetadata interface {
 
 // 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(),
@@ -1584,3 +1621,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/klogr/README.md b/klogr/README.md
index cfa5de6..c43fa81 100644
--- a/klogr/README.md
+++ b/klogr/README.md
@@ -5,4 +5,15 @@ in terms of Kubernetes' [klog](https://github.com/kubernetes/klog).  This
 provides a relatively minimalist API to logging in Go, backed by a well-proven
 implementation.
 
+Because klogr was implemented before klog itself added supported for
+structured logging, the default in klogr is to serialize key/value
+pairs with JSON and log the result as text messages via klog. This
+does not work well when klog itself forwards output to a structured
+logger.
+
+Therefore the recommended approach is to let klogr pass all log
+messages through to klog and deal with structured logging there. Just
+beware that the output of klog without a structured logger is meant to
+be human-readable, in contrast to the JSON-based traditional format.
+
 This is a BETA grade implementation.
diff --git a/klogr/klogr.go b/klogr/klogr.go
index 227d07b..a347a88 100644
--- a/klogr/klogr.go
+++ b/klogr/klogr.go
@@ -6,61 +6,71 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"runtime"
+	"github.com/go-logr/logr"
+	"k8s.io/klog/v2"
 	"sort"
 	"strings"
+)
 
-	"github.com/go-logr/logr"
-	"k8s.io/klog/v2"
+// Option is a functional option that reconfigures the logger created with New.
+type Option func(*klogger)
+
+// Format defines how log output is produced.
+type Format string
+
+const (
+	// FormatSerialize tells klogr to turn key/value pairs into text itself
+	// before invoking klog.
+	FormatSerialize Format = "Serialize"
+
+	// FormatKlog tells klogr to pass all text messages and key/value pairs
+	// directly to klog. Klog itself then serializes in a human-readable
+	// format and optionally passes on to a structure logging backend.
+	FormatKlog Format = "Klog"
 )
 
-// New returns a logr.Logger which is implemented by klog.
-func New() logr.Logger {
-	return klogger{
-		level:  0,
-		prefix: "",
-		values: nil,
+// WithFormat selects the output format.
+func WithFormat(format Format) Option {
+	return func(l *klogger) {
+		l.format = format
 	}
 }
 
-type klogger struct {
-	level  int
-	prefix string
-	values []interface{}
+// New returns a logr.Logger which serializes output itself
+// and writes it via klog.
+func New() logr.Logger {
+	return NewWithOptions(WithFormat(FormatSerialize))
 }
 
-func (l klogger) clone() klogger {
-	return klogger{
-		level:  l.level,
-		prefix: l.prefix,
-		values: copySlice(l.values),
+// NewWithOptions returns a logr.Logger which serializes as determined
+// by the WithFormat option and writes via klog. The default is
+// FormatKlog.
+func NewWithOptions(options ...Option) logr.Logger {
+	l := klogger{
+		level:  0,
+		prefix: "",
+		values: nil,
+		format: FormatKlog,
 	}
+	for _, option := range options {
+		option(&l)
+	}
+	return logr.New(&l)
 }
 
-func copySlice(in []interface{}) []interface{} {
-	out := make([]interface{}, len(in))
-	copy(out, in)
-	return out
+type klogger struct {
+	level     int
+	callDepth int
+	prefix    string
+	values    []interface{}
+	format    Format
 }
 
-// Magic string for intermediate frames that we should ignore.
-const autogeneratedFrameName = "<autogenerated>"
-
-// 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).
-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 deduplicates elements provided in multiple KV tuple
+// 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{} {
@@ -147,18 +157,25 @@ func pretty(value interface{}) string {
 	return strings.TrimSpace(string(buffer.Bytes()))
 }
 
-func (l klogger) Info(msg string, kvList ...interface{}) {
-	if l.Enabled() {
+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(framesToCaller(), l.prefix, " ", msgStr, " ", fixedStr, " ", userStr)
+		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{}) {
@@ -167,35 +184,44 @@ func (l klogger) Error(err error, msg string, kvList ...interface{}) {
 	if err != nil {
 		loggableErr = err.Error()
 	}
-	errStr := flatten("error", loggableErr)
-	trimmed := trimDuplicates(l.values, kvList)
-	fixedStr := flatten(trimmed[0]...)
-	userStr := flatten(trimmed[1]...)
-	klog.ErrorDepth(framesToCaller(), l.prefix, " ", msgStr, " ", errStr, " ", fixedStr, " ", userStr)
-}
-
-func (l klogger) V(level int) logr.Logger {
-	new := l.clone()
-	new.level = level
-	return new
+	switch l.format {
+	case FormatSerialize:
+		errStr := flatten("error", loggableErr)
+		trimmed := 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)
+		if l.prefix != "" {
+			msg = l.prefix + ": " + msg
+		}
+		klog.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.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.LogSink {
+	// Three slice args forces a copy.
+	n := len(l.values)
+	l.values = append(l.values[:n:n], kvList...)
+	return &l
 }
 
-func (l klogger) WithValues(kvList ...interface{}) logr.Logger {
-	new := l.clone()
-	new.values = append(new.values, kvList...)
-	return new
+func (l klogger) WithCallDepth(depth int) logr.LogSink {
+	l.callDepth += depth
+	return &l
 }
 
-var _ logr.Logger = klogger{}
+var _ logr.LogSink = &klogger{}
+var _ logr.CallDepthLogSink = &klogger{}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b027083..2aa4fb2 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:
@@ -18,6 +18,7 @@ jobs:
       run: |
         go get -t -v ./...
         go test -v -race ./...
+        cd hack/tools && go test -v -race ./...
   lint:
     runs-on: ubuntu-latest
     steps:
diff --git a/hack/tools/logcheck/main_test.go b/hack/tools/logcheck/main_test.go
new file mode 100644
index 0000000..3e0c08a
--- /dev/null
+++ b/hack/tools/logcheck/main_test.go
@@ -0,0 +1,49 @@
+/*
+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"
+)
+
+func TestAnalyzer(t *testing.T) {
+	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 0000000..346410d
--- /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/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
new file mode 100644
index 0000000..19c254a
--- /dev/null
+++ b/hack/tools/logcheck/testdata/src/doNotAllowUnstructuredLogs/doNotAllowUnstructuredLogs.go
@@ -0,0 +1,64 @@
+/*
+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 `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`
+	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`
+
+}
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
new file mode 100644
index 0000000..63bfafe
--- /dev/null
+++ b/hack/tools/logcheck/testdata/src/k8s.io/klog/v2/klog.go
@@ -0,0 +1,182 @@
+/*
+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
+
+// 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
+
+func V(level Level) Verbose {
+
+	return Verbose{enabled: 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{}) {
+}
diff --git a/klog_test.go b/klog_test.go
index bf7a063..9a44c6b 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)
 	}
 }
 
@@ -518,6 +518,7 @@ func TestOpenAppendOnStart(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	defer os.Remove(f.Name())
 	logging.logFile = f.Name()
 
 	// Erase files created by prior tests,
@@ -620,13 +621,33 @@ func BenchmarkHeaderWithDir(b *testing.B) {
 	}
 }
 
+// Ensure that benchmarks have side effects to avoid compiler optimization
+var result ObjectRef
+
+func BenchmarkKRef(b *testing.B) {
+	var r ObjectRef
+	for i := 0; i < b.N; i++ {
+		r = KRef("namespace", "name")
+	}
+	result = r
+}
+
+func BenchmarkKObj(b *testing.B) {
+	a := kMetadataMock{name: "a", ns: "a"}
+	var r ObjectRef
+	for i := 0; i < b.N; i++ {
+		r = KObj(&a)
+	}
+	result = r
+}
+
 func BenchmarkLogs(b *testing.B) {
 	setFlags()
 	defer logging.swap(logging.newBuffers())
 
 	testFile, err := ioutil.TempFile("", "test.log")
 	if err != nil {
-		b.Error("unable to create temporary file")
+		b.Fatal("unable to create temporary file")
 	}
 	defer os.Remove(testFile.Name())
 
@@ -733,6 +754,11 @@ func TestInfoObjectRef(t *testing.T) {
 			},
 			want: "test-name",
 		},
+		{
+			name: "empty",
+			ref:  ObjectRef{},
+			want: "",
+		},
 	}
 
 	for _, tt := range tests {
@@ -745,14 +771,25 @@ func TestInfoObjectRef(t *testing.T) {
 	}
 }
 
-type mockKmeta struct {
+type kMetadataMock struct {
 	name, ns string
 }
 
-func (m mockKmeta) GetName() string {
+func (m kMetadataMock) GetName() string {
 	return m.name
 }
-func (m mockKmeta) GetNamespace() string {
+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
 }
 
@@ -762,9 +799,29 @@ func TestKObj(t *testing.T) {
 		obj  KMetadata
 		want ObjectRef
 	}{
+		{
+			name: "nil passed as pointer KMetadata implementation",
+			obj:  (*ptrKMetadataMock)(nil),
+			want: ObjectRef{},
+		},
+		{
+			name: "empty struct passed as non-pointer KMetadata implementation",
+			obj:  kMetadataMock{},
+			want: ObjectRef{},
+		},
+		{
+			name: "nil pointer passed to non-pointer KMetadata implementation",
+			obj:  (*kMetadataMock)(nil),
+			want: ObjectRef{},
+		},
+		{
+			name: "nil",
+			obj:  nil,
+			want: ObjectRef{},
+		},
 		{
 			name: "with ns",
-			obj:  mockKmeta{"test-name", "test-ns"},
+			obj:  &kMetadataMock{"test-name", "test-ns"},
 			want: ObjectRef{
 				Name:      "test-name",
 				Namespace: "test-ns",
@@ -772,7 +829,7 @@ func TestKObj(t *testing.T) {
 		},
 		{
 			name: "without ns",
-			obj:  mockKmeta{"test-name", ""},
+			obj:  &kMetadataMock{"test-name", ""},
 			want: ObjectRef{
 				Name: "test-name",
 			},
@@ -822,7 +879,7 @@ func TestKRef(t *testing.T) {
 	}
 }
 
-// Test that InfoS works as advertised.
+// Test that InfoS and InfoSDepth work as advertised.
 func TestInfoS(t *testing.T) {
 	setFlags()
 	defer logging.swap(logging.newBuffers())
@@ -857,17 +914,23 @@ func TestInfoS(t *testing.T) {
 		},
 	}
 
-	for _, data := range testDataInfo {
-		logging.file[infoLog] = &flushBuffer{}
-		InfoS(data.msg, data.keysValues...)
-		var line int
-		n, err := fmt.Sscanf(contents(infoLog), data.format, &line)
-		if n != 1 || err != nil {
-			t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
-		}
-		want := fmt.Sprintf(data.format, line)
-		if contents(infoLog) != want {
-			t.Errorf("InfoS has wrong format: \n got:\t%s\nwant:\t%s", contents(infoLog), want)
+	functions := []func(msg string, keyAndValues ...interface{}){
+		InfoS,
+		myInfoS,
+	}
+	for _, f := range functions {
+		for _, data := range testDataInfo {
+			logging.file[infoLog] = &flushBuffer{}
+			f(data.msg, data.keysValues...)
+			var line int
+			n, err := fmt.Sscanf(contents(infoLog), data.format, &line)
+			if n != 1 || err != nil {
+				t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
+			}
+			want := fmt.Sprintf(data.format, line)
+			if contents(infoLog) != want {
+				t.Errorf("InfoS has wrong format: \n got:\t%s\nwant:\t%s", contents(infoLog), want)
+			}
 		}
 	}
 }
@@ -930,7 +993,7 @@ func TestVInfoS(t *testing.T) {
 	}
 }
 
-// Test that ErrorS works as advertised.
+// Test that ErrorS and ErrorSDepth work as advertised.
 func TestErrorS(t *testing.T) {
 	setFlags()
 	defer logging.swap(logging.newBuffers())
@@ -939,16 +1002,38 @@ func TestErrorS(t *testing.T) {
 	}
 	logging.logFile = ""
 	pid = 1234
-	ErrorS(fmt.Errorf("update status failed"), "Failed to update pod status", "pod", "kubedns")
-	var line int
-	format := "E0102 15:04:05.067890    1234 klog_test.go:%d] \"Failed to update pod status\" err=\"update status failed\" pod=\"kubedns\"\n"
-	n, err := fmt.Sscanf(contents(errorLog), format, &line)
-	if n != 1 || err != nil {
-		t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(errorLog))
-	}
-	want := fmt.Sprintf(format, line)
-	if contents(errorLog) != want {
-		t.Errorf("ErrorS has wrong format: \n got:\t%s\nwant:\t%s", contents(errorLog), want)
+
+	functions := []func(err error, msg string, keyAndValues ...interface{}){
+		ErrorS,
+		myErrorS,
+	}
+	for _, f := range functions {
+		var errorList = []struct {
+			err    error
+			format string
+		}{
+			{
+				err:    fmt.Errorf("update status failed"),
+				format: "E0102 15:04:05.067890    1234 klog_test.go:%d] \"Failed to update pod status\" err=\"update status failed\" pod=\"kubedns\"\n",
+			},
+			{
+				err:    nil,
+				format: "E0102 15:04:05.067890    1234 klog_test.go:%d] \"Failed to update pod status\" pod=\"kubedns\"\n",
+			},
+		}
+		for _, e := range errorList {
+			logging.file[errorLog] = &flushBuffer{}
+			f(e.err, "Failed to update pod status", "pod", "kubedns")
+			var line int
+			n, err := fmt.Sscanf(contents(errorLog), e.format, &line)
+			if n != 1 || err != nil {
+				t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(errorLog))
+			}
+			want := fmt.Sprintf(e.format, line)
+			if contents(errorLog) != want {
+				t.Errorf("ErrorS has wrong format: \n got:\t%s\nwant:\t%s", contents(errorLog), want)
+			}
+		}
 	}
 }
 
@@ -982,6 +1067,14 @@ func TestKvListFormat(t *testing.T) {
 			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{}{"pod", "kubedns", "maps", map[string]int{"three": 4}},
 			want:       " pod=\"kubedns\" maps=map[three:4]",
@@ -995,13 +1088,25 @@ func TestKvListFormat(t *testing.T) {
 			want:       " pod=\"kubedns\" status=\"ready\"",
 		},
 		{
-			keysValues: []interface{}{"pod", KObj(mockKmeta{"test-name", "test-ns"}), "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(mockKmeta{"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\"",
+		},
 	}
 
 	for _, d := range testKVList {
@@ -1270,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...)
@@ -1337,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...)
@@ -1350,6 +1457,171 @@ func TestErrorSWithLogr(t *testing.T) {
 	}
 }
 
+func TestCallDepthLogr(t *testing.T) {
+	logger := &callDepthTestLogr{}
+	logger.resetCallDepth()
+
+	testCases := []struct {
+		name  string
+		logFn func()
+	}{
+		{
+			name:  "Info log",
+			logFn: func() { Info("info log") },
+		},
+		{
+			name:  "InfoDepth log",
+			logFn: func() { InfoDepth(0, "infodepth log") },
+		},
+		{
+			name:  "InfoSDepth log",
+			logFn: func() { InfoSDepth(0, "infoSDepth log") },
+		},
+		{
+			name:  "Warning log",
+			logFn: func() { Warning("warning log") },
+		},
+		{
+			name:  "WarningDepth log",
+			logFn: func() { WarningDepth(0, "warningdepth log") },
+		},
+		{
+			name:  "Error log",
+			logFn: func() { Error("error log") },
+		},
+		{
+			name:  "ErrorDepth log",
+			logFn: func() { ErrorDepth(0, "errordepth log") },
+		},
+		{
+			name:  "ErrorSDepth log",
+			logFn: func() { ErrorSDepth(0, errors.New("some error"), "errorSDepth log") },
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			l := logr.New(logger)
+			SetLogger(l)
+			defer ClearLogger()
+			defer logger.reset()
+			defer logger.resetCallDepth()
+
+			// Keep these lines together.
+			_, wantFile, wantLine, _ := runtime.Caller(0)
+			tc.logFn()
+			wantLine++
+
+			if len(logger.entries) != 1 {
+				t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+			}
+			checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+		})
+	}
+}
+
+func TestCallDepthLogrInfoS(t *testing.T) {
+	logger := &callDepthTestLogr{}
+	logger.resetCallDepth()
+	l := logr.New(logger)
+	SetLogger(l)
+
+	// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
+	logFunc := func() {
+		InfoS("infoS log")
+	}
+
+	// Keep these lines together.
+	_, wantFile, wantLine, _ := runtime.Caller(0)
+	logFunc()
+	wantLine++
+
+	if len(logger.entries) != 1 {
+		t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+	}
+	checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+}
+
+func TestCallDepthLogrErrorS(t *testing.T) {
+	logger := &callDepthTestLogr{}
+	logger.resetCallDepth()
+	l := logr.New(logger)
+	SetLogger(l)
+
+	// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
+	logFunc := func() {
+		ErrorS(errors.New("some error"), "errorS log")
+	}
+
+	// Keep these lines together.
+	_, wantFile, wantLine, _ := runtime.Caller(0)
+	logFunc()
+	wantLine++
+
+	if len(logger.entries) != 1 {
+		t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+	}
+	checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+}
+
+func TestCallDepthLogrGoLog(t *testing.T) {
+	logger := &callDepthTestLogr{}
+	logger.resetCallDepth()
+	l := logr.New(logger)
+	SetLogger(l)
+	CopyStandardLogTo("INFO")
+
+	// Add wrapper to ensure callDepthTestLogr +2 offset is correct.
+	logFunc := func() {
+		stdLog.Print("some log")
+	}
+
+	// Keep these lines together.
+	_, wantFile, wantLine, _ := runtime.Caller(0)
+	logFunc()
+	wantLine++
+
+	if len(logger.entries) != 1 {
+		t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+	}
+	checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+	fmt.Println(logger.entries[0])
+}
+
+// Test callDepthTestLogr logs the expected offsets.
+func TestCallDepthTestLogr(t *testing.T) {
+	logger := &callDepthTestLogr{}
+	logger.resetCallDepth()
+
+	logFunc := func() {
+		logger.Info(0, "some info log")
+	}
+	// Keep these lines together.
+	_, wantFile, wantLine, _ := runtime.Caller(0)
+	logFunc()
+	wantLine++
+
+	if len(logger.entries) != 1 {
+		t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+	}
+	checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+
+	logger.reset()
+
+	logFunc = func() {
+		logger.Error(errors.New("error"), "some error log")
+	}
+	// Keep these lines together.
+	_, wantFile, wantLine, _ = runtime.Caller(0)
+	logFunc()
+	wantLine++
+
+	if len(logger.entries) != 1 {
+		t.Errorf("expected a single log entry to be generated, got %d", len(logger.entries))
+	}
+	checkLogrEntryCorrectCaller(t, wantFile, wantLine, logger.entries[0])
+}
+
 type testLogr struct {
 	entries []testLogrEntry
 	mutex   sync.Mutex
@@ -1368,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{
@@ -1389,11 +1661,77 @@ 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
+	callDepth int
+}
+
+func (l *callDepthTestLogr) resetCallDepth() {
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+	l.callDepth = 0
+}
+
+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 + 1
+	return l
+}
+
+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
+	// test case.
+	_, file, line, _ := runtime.Caller(l.callDepth + 2)
+	l.entries = append(l.entries, testLogrEntry{
+		severity:      infoLog,
+		msg:           msg,
+		keysAndValues: append([]interface{}{file, line}, keysAndValues...),
+	})
+}
+
+func (l *callDepthTestLogr) Error(err error, msg string, keysAndValues ...interface{}) {
+	l.mutex.Lock()
+	defer l.mutex.Unlock()
+	// Add 2 to depth for the wrapper function caller and for invocation in
+	// test case.
+	_, file, line, _ := runtime.Caller(l.callDepth + 2)
+	l.entries = append(l.entries, testLogrEntry{
+		severity:      errorLog,
+		msg:           msg,
+		keysAndValues: append([]interface{}{file, line}, keysAndValues...),
+		err:           err,
+	})
+}
+
+var _ logr.LogSink = &callDepthTestLogr{}
+var _ logr.CallDepthLogSink = &callDepthTestLogr{}
+
+func checkLogrEntryCorrectCaller(t *testing.T, wantFile string, wantLine int, entry testLogrEntry) {
+	t.Helper()
+
+	want := fmt.Sprintf("%s:%d", wantFile, wantLine)
+	// Log fields contain file and line number as first elements.
+	got := fmt.Sprintf("%s:%d", entry.keysAndValues[0], entry.keysAndValues[1])
+
+	if want != got {
+		t.Errorf("expected file and line %q but got %q", want, got)
+	}
 }
 
 // existedFlag contains all existed flag, without KlogPrefix
@@ -1428,3 +1766,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))
+			}
+		})
+	}
+}
diff --git a/klog_wrappers_test.go b/klog_wrappers_test.go
new file mode 100644
index 0000000..10325f5
--- /dev/null
+++ b/klog_wrappers_test.go
@@ -0,0 +1,27 @@
+// Copyright 2020 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
+
+// These helper functions must be in a separate source file because the
+// tests in klog_test.go compare the logged source code file name against
+// "klog_test.go". "klog_wrappers_test.go" must *not* be logged.
+
+func myInfoS(msg string, keyAndValues ...interface{}) {
+	InfoSDepth(1, msg, keyAndValues...)
+}
+
+func myErrorS(err error, msg string, keyAndValues ...interface{}) {
+	ErrorSDepth(1, err, msg, keyAndValues...)
+}
diff --git a/klogr/calldepth-test/call_depth_helper_test.go b/klogr/calldepth-test/call_depth_helper_test.go
new file mode 100644
index 0000000..03326bf
--- /dev/null
+++ b/klogr/calldepth-test/call_depth_helper_test.go
@@ -0,0 +1,16 @@
+package calldepth
+
+import (
+	"github.com/go-logr/logr"
+)
+
+// Putting these functions into a separate file makes it possible to validate that
+// their source code file is *not* logged because of WithCallDepth(1).
+
+func myInfo(l logr.Logger, msg string) {
+	l.WithCallDepth(2).Info(msg)
+}
+
+func myInfo2(l logr.Logger, msg string) {
+	myInfo(l.WithCallDepth(2), msg)
+}
diff --git a/klogr/calldepth-test/call_depth_main_test.go b/klogr/calldepth-test/call_depth_main_test.go
new file mode 100644
index 0000000..6d7e59f
--- /dev/null
+++ b/klogr/calldepth-test/call_depth_main_test.go
@@ -0,0 +1,57 @@
+// Package calldepth does black-box testing.
+//
+// Another intentional effect is that "go test" compiles
+// this into a separate binary which we need because
+// we have to configure klog differently that TestOutput.
+
+package calldepth
+
+import (
+	"bytes"
+	"flag"
+	"strings"
+	"testing"
+
+	"k8s.io/klog/v2"
+	"k8s.io/klog/v2/klogr"
+)
+
+func TestCallDepth(t *testing.T) {
+	klog.InitFlags(nil)
+	flag.CommandLine.Set("v", "10")
+	flag.CommandLine.Set("skip_headers", "false")
+	flag.CommandLine.Set("logtostderr", "false")
+	flag.CommandLine.Set("alsologtostderr", "false")
+	flag.CommandLine.Set("stderrthreshold", "10")
+	flag.Parse()
+
+	t.Run("call-depth", func(t *testing.T) {
+		logr := klogr.New()
+
+		// hijack the klog output
+		tmpWriteBuffer := bytes.NewBuffer(nil)
+		klog.SetOutput(tmpWriteBuffer)
+
+		validate := func(t *testing.T) {
+			output := tmpWriteBuffer.String()
+			if !strings.Contains(output, "call_depth_main_test.go:") {
+				t.Fatalf("output should have contained call_depth_main_test.go, got instead: %s", output)
+			}
+		}
+
+		t.Run("direct", func(t *testing.T) {
+			logr.Info("hello world")
+			validate(t)
+		})
+
+		t.Run("indirect", func(t *testing.T) {
+			myInfo(logr, "hello world")
+			validate(t)
+		})
+
+		t.Run("nested", func(t *testing.T) {
+			myInfo2(logr, "hello world")
+			validate(t)
+		})
+	})
+}
diff --git a/klogr/klogr_test.go b/klogr/klogr_test.go
index 38b4410..7775b43 100644
--- a/klogr/klogr_test.go
+++ b/klogr/klogr_test.go
@@ -13,112 +13,212 @@ import (
 	"github.com/go-logr/logr"
 )
 
-func TestInfo(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.Parse()
+const (
+	formatDefault = "Default"
+	formatNew     = "New"
+)
 
+func testOutput(t *testing.T, format string) {
+	new := func() logr.Logger {
+		switch format {
+		case formatNew:
+			return New()
+		case formatDefault:
+			return NewWithOptions()
+		default:
+			return NewWithOptions(WithFormat(Format(format)))
+		}
+	}
 	tests := map[string]struct {
-		klogr          logr.Logger
-		text           string
-		keysAndValues  []interface{}
-		expectedOutput string
+		klogr              logr.Logger
+		text               string
+		keysAndValues      []interface{}
+		err                error
+		expectedOutput     string
+		expectedKlogOutput string
 	}{
 		"should log with values passed to keysAndValues": {
-			klogr:         New().V(0),
+			klogr:         new().V(0),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue"
+`,
+			expectedKlogOutput: `"test" akey="avalue"
+`,
+		},
+		"should log with name and values passed to keysAndValues": {
+			klogr:         new().V(0).WithName("me"),
+			text:          "test",
+			keysAndValues: []interface{}{"akey", "avalue"},
+			expectedOutput: `me "msg"="test"  "akey"="avalue"
+`,
+			expectedKlogOutput: `"me: test" akey="avalue"
+`,
+		},
+		"should log with multiple names and values passed to keysAndValues": {
+			klogr:         new().V(0).WithName("hello").WithName("world"),
+			text:          "test",
+			keysAndValues: []interface{}{"akey", "avalue"},
+			expectedOutput: `hello/world "msg"="test"  "akey"="avalue"
+`,
+			expectedKlogOutput: `"hello/world: test" akey="avalue"
 `,
 		},
 		"should not print duplicate keys with the same value": {
-			klogr:         New().V(0),
+			klogr:         new().V(0),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue"
+`,
+			expectedKlogOutput: `"test" akey="avalue"
 `,
 		},
 		"should only print the last duplicate key when the values are passed to Info": {
-			klogr:         New().V(0),
+			klogr:         new().V(0),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue2"
+`,
+			expectedKlogOutput: `"test" 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"),
+			klogr:         new().WithValues("akey", "avalue"),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue"
+`,
+			expectedKlogOutput: `"test" akey="avalue"
+`,
+		},
+		"should sort within logger and parameter key/value pairs in the default format and dump the logger pairs first": {
+			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"
+`,
+			expectedKlogOutput: `"test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4"
 `,
 		},
 		"should only print the key passed to Info when one is already set on the logger": {
-			klogr:         New().WithValues("akey", "avalue"),
+			klogr:         new().WithValues("akey", "avalue"),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue2"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue2"
+`,
+			expectedKlogOutput: `"test" akey="avalue2"
 `,
 		},
 		"should correctly handle odd-numbers of KVs": {
+			klogr:         new(),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue", "akey2"},
 			expectedOutput: ` "msg"="test"  "akey"="avalue" "akey2"=null
+`,
+			expectedKlogOutput: `"test" akey="avalue" akey2=<nil>
 `,
 		},
 		"should correctly html characters": {
+			klogr:         new(),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "<&>"},
 			expectedOutput: ` "msg"="test"  "akey"="<&>"
+`,
+			expectedKlogOutput: `"test" akey="<&>"
 `,
 		},
 		"should correctly handle odd-numbers of KVs in both log values and Info args": {
-			klogr:         New().WithValues("basekey1", "basevar1", "basekey2"),
+			klogr:         new().WithValues("basekey1", "basevar1", "basekey2"),
 			text:          "test",
 			keysAndValues: []interface{}{"akey", "avalue", "akey2"},
 			expectedOutput: ` "msg"="test" "basekey1"="basevar1" "basekey2"=null "akey"="avalue" "akey2"=null
+`,
+			expectedKlogOutput: `"test" basekey1="basevar1" basekey2=<nil> akey="avalue" akey2=<nil>
 `,
 		},
 		"should correctly print regular error types": {
-			klogr:         New().V(0),
+			klogr:         new().V(0),
 			text:          "test",
 			keysAndValues: []interface{}{"err", errors.New("whoops")},
 			expectedOutput: ` "msg"="test"  "err"="whoops"
+`,
+			expectedKlogOutput: `"test" err="whoops"
 `,
 		},
-		"should use MarshalJSON if an error type implements it": {
-			klogr:         New().V(0),
+		"should use MarshalJSON in the default format if an error type implements it": {
+			klogr:         new().V(0),
 			text:          "test",
 			keysAndValues: []interface{}{"err", &customErrorJSON{"whoops"}},
 			expectedOutput: ` "msg"="test"  "err"="WHOOPS"
+`,
+			expectedKlogOutput: `"test" err="whoops"
+`,
+		},
+		"should correctly print regular error types when using logr.Error": {
+			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"
 `,
 		},
 	}
 	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)
 
-			klogr.Info(test.text, test.keysAndValues...)
+			if test.err != nil {
+				test.klogr.Error(test.err, test.text, test.keysAndValues...)
+			} else {
+				test.klogr.Info(test.text, test.keysAndValues...)
+			}
+
 			// call Flush to ensure the text isn't still buffered
 			klog.Flush()
 
 			actual := tmpWriteBuffer.String()
-			if actual != test.expectedOutput {
-				t.Errorf("expected %q did not match actual %q", test.expectedOutput, actual)
+			expectedOutput := test.expectedOutput
+			if format == string(FormatKlog) || format == formatDefault {
+				expectedOutput = test.expectedKlogOutput
+			}
+			if actual != expectedOutput {
+				t.Errorf("expected %q did not match actual %q", expectedOutput, actual)
 			}
 		})
 	}
 }
 
+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()
+
+	formats := []string{
+		formatNew,
+		formatDefault,
+		string(FormatSerialize),
+		string(FormatKlog),
+	}
+	for _, format := range formats {
+		t.Run(format, func(t *testing.T) {
+			testOutput(t, format)
+		})
+	}
+}
+
 type customErrorJSON struct {
 	s string
 }