Repository: kubernetes/klog
Pull request #296 (2022-02-16 08:03:47)

contextual logging
**What this PR does / why we need it**:

Contextual logging enables several new use cases as shown in https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/3077-contextual-logging.

This PR is an implementation of the logging side of the [revised KEP (currently pending in review)](https://github.com/kubernetes/enhancements/pull/3222). A PR with the logcheck changes will be filed separately.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Related-to: https://github.com/kubernetes/enhancements/issues/3077

**Special notes for your reviewer**:

We probably should merge the KEP change first.

This PR is best looked at commit-by-commit.

**Release note**:
```release-note
Additional API calls for [contextual logging](https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/3077-contextual-logging).
```

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/klog/pull/296#" title="Author self-approved">pohly</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **serathius** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @serathius` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fklog).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/klog/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["serathius"]} -->
dims: /assign @thockin @serathius 

Patch:
diff --git a/contextual.go b/contextual.go
new file mode 100644
index 0000000..6749267
--- /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/examples/go.mod b/examples/go.mod
index b7750ac..9c16484 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -6,7 +6,6 @@ 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 3326ef4..b7fda52 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -1,7 +1,6 @@
 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=
@@ -22,81 +21,38 @@ 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 6385c6c..31aefba 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 ef73139..919fbad 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/imports.go b/imports.go
new file mode 100644
index 0000000..43cd081
--- /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
+)
diff --git a/k8s_references.go b/k8s_references.go
new file mode 100644
index 0000000..db58f8b
--- /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 0b235f4..e2eb0f5 100644
--- a/klog.go
+++ b/klog.go
@@ -81,7 +81,6 @@ import (
 	"math"
 	"os"
 	"path/filepath"
-	"reflect"
 	"runtime"
 	"strconv"
 	"strings"
@@ -94,7 +93,6 @@ 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
@@ -381,7 +379,7 @@ type flushSyncWriter interface {
 	io.Writer
 }
 
-// init sets up the defaults.
+// init sets up the defaults and runs flushDaemon.
 func init() {
 	logging.stderrThreshold = severityValue{
 		Severity: severity.ErrorLog, // Default stderrThreshold is ERROR.
@@ -396,7 +394,7 @@ func init() {
 	logging.addDirHeader = false
 	logging.skipLogHeaders = false
 	logging.oneOutput = false
-	logging.flushD = newFlushDaemon(flushInterval, logging.lockAndFlushAll, nil)
+	go logging.flushDaemon()
 }
 
 // InitFlags is for explicitly initializing the flags.
@@ -448,8 +446,6 @@ 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.
@@ -487,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
 
@@ -689,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)
 }
@@ -711,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()
@@ -794,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))
 		}
@@ -1025,7 +990,6 @@ 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.
@@ -1046,86 +1010,10 @@ func (l *loggingT) createFiles(sev severity.Severity) error {
 const flushInterval = 5 * time.Second
 
 // flushDaemon periodically flushes the log file buffers.
-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
+func (l *loggingT) flushDaemon() {
+	for range time.NewTicker(flushInterval).C {
+		l.lockAndFlushAll()
 	}
-
-	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.
@@ -1192,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
 }
 
@@ -1230,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}
 }
 
@@ -1352,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.
@@ -1381,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.
@@ -1423,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.
@@ -1508,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.
@@ -1564,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,
@@ -1616,73 +1504,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
-}
diff --git a/klogr.go b/klogr.go
new file mode 100644
index 0000000..cdb3834
--- /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{}


Test patch:
diff --git a/contextual_test.go b/contextual_test.go
new file mode 100644
index 0000000..fc95f05
--- /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/flushing/flushing_test.go b/examples/flushing/flushing_test.go
deleted file mode 100644
index 3d46fbc..0000000
--- a/examples/flushing/flushing_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-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/output_test/output_test.go b/examples/output_test/output_test.go
index 0624e3a..b417012 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:<LINE>] "odd arguments" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:<LINE>","msg":"odd arguments","v":0,"akey":"avalue","akey2":"(MISSING)"}
+`,
+
+		`I output.go:<LINE>] "both odd" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
+`: `{"caller":"test/output.go:<LINE>","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_test.go b/klog_test.go
index 95553d5..6e16676 100644
--- a/klog_test.go
+++ b/klog_test.go
@@ -39,7 +39,6 @@ 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
@@ -379,11 +378,10 @@ func TestSetOutputDataRace(t *testing.T) {
 	setFlags()
 	defer logging.swap(logging.newBuffers())
 	var wg sync.WaitGroup
-	var daemons []*flushDaemon
 	for i := 1; i <= 50; i++ {
-		daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil)
-		daemon.run()
-		daemons = append(daemons, daemon)
+		go func() {
+			logging.flushDaemon()
+		}()
 	}
 	for i := 1; i <= 50; i++ {
 		wg.Add(1)
@@ -393,9 +391,9 @@ func TestSetOutputDataRace(t *testing.T) {
 		}()
 	}
 	for i := 1; i <= 50; i++ {
-		daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil)
-		daemon.run()
-		daemons = append(daemons, daemon)
+		go func() {
+			logging.flushDaemon()
+		}()
 	}
 	for i := 1; i <= 50; i++ {
 		wg.Add(1)
@@ -405,14 +403,11 @@ func TestSetOutputDataRace(t *testing.T) {
 		}()
 	}
 	for i := 1; i <= 50; i++ {
-		daemon := newFlushDaemon(time.Second, logging.lockAndFlushAll, nil)
-		daemon.run()
-		daemons = append(daemons, daemon)
+		go func() {
+			logging.flushDaemon()
+		}()
 	}
 	wg.Wait()
-	for _, d := range daemons {
-		d.stop()
-	}
 }
 
 func TestLogToOutput(t *testing.T) {
@@ -1857,63 +1852,3 @@ 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")
-	}
-}
diff --git a/ktesting/example/example_test.go b/ktesting/example/example_test.go
new file mode 100644
index 0000000..79eeef4
--- /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 0000000..c1fa145
--- /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 0000000..8cb13c4
--- /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 0000000..d3029ae
--- /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 0000000..5269f89
--- /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 0000000..430ece7
--- /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"))
+}