Skip to content

Commit 2a5cfa4

Browse files
committed
make logr understand kubernetes object
1 parent 1118057 commit 2a5cfa4

File tree

3 files changed

+189
-4
lines changed

3 files changed

+189
-4
lines changed

pkg/log/logr/logr.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package logr contains helpers for setting up a Kubernetes object aware logr.Logger.
18+
package logr
19+
20+
import (
21+
"reflect"
22+
23+
"github.com/go-logr/logr"
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
)
27+
28+
var _ logr.LogSink = (*KubeAwareSink)(nil)
29+
30+
// KubeAwareSink is a logr.LogSink that understands Kubernetes objects.
31+
type KubeAwareSink struct {
32+
sink logr.LogSink
33+
}
34+
35+
// KubeAware wraps a logr.logger to make it aware of Kubernetes objects.
36+
func KubeAware(logger logr.Logger) logr.Logger {
37+
return logr.New(
38+
&KubeAwareSink{logger.GetSink()},
39+
)
40+
}
41+
42+
// Init implements logr.LogSink.
43+
func (k *KubeAwareSink) Init(info logr.RuntimeInfo) {
44+
k.sink.Init(info)
45+
}
46+
47+
// Enabled implements logr.LogSink.
48+
func (k *KubeAwareSink) Enabled(level int) bool {
49+
return k.sink.Enabled(level)
50+
}
51+
52+
// Info implements logr.LogSink.
53+
func (k *KubeAwareSink) Info(level int, msg string, keysAndValues ...interface{}) {
54+
k.sink.Info(level, msg, k.wrapKeyAndValues(keysAndValues)...)
55+
}
56+
57+
// Error implements logr.LogSink.
58+
func (k *KubeAwareSink) Error(err error, msg string, keysAndValues ...interface{}) {
59+
k.sink.Error(err, msg, k.wrapKeyAndValues(keysAndValues)...)
60+
}
61+
62+
// WithValues implements logr.LogSink.
63+
func (k *KubeAwareSink) WithValues(keysAndValues ...interface{}) logr.LogSink {
64+
return &KubeAwareSink{
65+
sink: k.sink.WithValues(k.wrapKeyAndValues(keysAndValues)...),
66+
}
67+
}
68+
69+
// WithName implements logr.LogSink.
70+
func (k *KubeAwareSink) WithName(name string) logr.LogSink {
71+
return &KubeAwareSink{
72+
sink: k.sink.WithName(name),
73+
}
74+
}
75+
76+
// wrapKeyAndValues replaces Kubernetes objects with [kubeObjectWrapper].
77+
func (k *KubeAwareSink) wrapKeyAndValues(keysAndValues []interface{}) []interface{} {
78+
result := make([]interface{}, len(keysAndValues))
79+
for i, item := range keysAndValues {
80+
if i%2 == 0 {
81+
// item is key, no need to resolve
82+
result[i] = item
83+
continue
84+
}
85+
86+
switch val := item.(type) {
87+
case runtime.Object:
88+
result[i] = &kubeObjectWrapper{obj: val}
89+
default:
90+
result[i] = item
91+
}
92+
}
93+
return result
94+
}
95+
96+
var _ logr.Marshaler = (*kubeObjectWrapper)(nil)
97+
98+
// kubeObjectWrapper is a wrapper around runtime.Object that implements logr.Marshaler.
99+
type kubeObjectWrapper struct {
100+
obj runtime.Object
101+
}
102+
103+
// MarshalLog implements logr.Marshaler.
104+
// The implementation mirrors the behavior of kubeObjectWrapper.MarshalLogObject.
105+
func (w *kubeObjectWrapper) MarshalLog() interface{} {
106+
result := make(map[string]string)
107+
108+
if reflect.ValueOf(w.obj).IsNil() {
109+
return "got nil for runtime.Object"
110+
}
111+
112+
if gvk := w.obj.GetObjectKind().GroupVersionKind(); gvk.Version != "" {
113+
result["apiVersion"] = gvk.GroupVersion().String()
114+
result["kind"] = gvk.Kind
115+
}
116+
117+
objMeta, err := meta.Accessor(w.obj)
118+
if err != nil {
119+
return result
120+
}
121+
122+
if ns := objMeta.GetNamespace(); ns != "" {
123+
result["namespace"] = ns
124+
}
125+
result["name"] = objMeta.GetName()
126+
return result
127+
}

pkg/log/zap/zap.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"github.com/go-logr/zapr"
2929
"go.uber.org/zap"
3030
"go.uber.org/zap/zapcore"
31+
32+
logrhelper "sigs.k8s.io/controller-runtime/pkg/log/logr"
3133
)
3234

3335
// EncoderConfigOption is a function that can modify a `zapcore.EncoderConfig`.
@@ -36,11 +38,14 @@ type EncoderConfigOption func(*zapcore.EncoderConfig)
3638
// NewEncoderFunc is a function that creates an Encoder using the provided EncoderConfigOptions.
3739
type NewEncoderFunc func(...EncoderConfigOption) zapcore.Encoder
3840

39-
// New returns a brand new Logger configured with Opts. It
40-
// uses KubeAwareEncoder which adds Type information and
41-
// Namespace/Name to the log.
41+
// New returns a brand new Logger configured with Opts.
42+
// It uses [logrhelper.KubeAware] to make the logger Kubernetes-aware,
43+
// meaning that for Kubernetes objects, the logger will by default digest only the metadata.
4244
func New(opts ...Opts) logr.Logger {
43-
return zapr.NewLogger(NewRaw(opts...))
45+
zrl := zapr.NewLogger(NewRaw(opts...))
46+
// Make sure the created logr.logger is Kubernetes-aware.
47+
// See https://github.com/kubernetes-sigs/controller-runtime/issues/1290 for the background.
48+
return logrhelper.KubeAware(zrl)
4449
}
4550

4651
// Opts allows to manipulate Options.

pkg/log/zap/zap_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,59 @@ var _ = Describe("Zap logger setup", func() {
264264
outRaw := logOut.Bytes()
265265
Expect(string(outRaw)).Should(ContainSubstring("got nil for runtime.Object"))
266266
})
267+
268+
It("should log a standard namespaced when using logrLogger.WithValues", func() {
269+
name := types.NamespacedName{Name: "some-pod", Namespace: "some-ns"}
270+
logger.WithValues("thing", name).Info("here's a kubernetes object")
271+
272+
outRaw := logOut.Bytes()
273+
res := map[string]interface{}{}
274+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
275+
276+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
277+
"name": name.Name,
278+
"namespace": name.Namespace,
279+
}))
280+
})
281+
282+
It("should log a standard Kubernetes objects when using logrLogger.WithValues", func() {
283+
node := &corev1.Node{}
284+
node.Name = "a-node"
285+
node.APIVersion = "v1"
286+
node.Kind = "Node"
287+
logger.WithValues("thing", node).Info("here's a kubernetes object")
288+
289+
outRaw := logOut.Bytes()
290+
res := map[string]interface{}{}
291+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
292+
293+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
294+
"name": node.Name,
295+
"apiVersion": node.APIVersion,
296+
"kind": node.Kind,
297+
}))
298+
})
299+
300+
It("should log a standard unstructured Kubernetes object when using logrLogger.WithValues", func() {
301+
pod := &unstructured.Unstructured{
302+
Object: map[string]interface{}{
303+
"metadata": map[string]interface{}{
304+
"name": "a-pod",
305+
"namespace": "a-ns",
306+
},
307+
},
308+
}
309+
logger.WithValues("thing", pod).Info("here's a kubernetes object")
310+
311+
outRaw := logOut.Bytes()
312+
res := map[string]interface{}{}
313+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
314+
315+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
316+
"name": "a-pod",
317+
"namespace": "a-ns",
318+
}))
319+
})
267320
}
268321

269322
Context("with logger created using New", func() {

0 commit comments

Comments
 (0)