diff --git a/pkg/log/logr/logr.go b/pkg/log/logr/logr.go new file mode 100644 index 0000000000..7e9f9f1f90 --- /dev/null +++ b/pkg/log/logr/logr.go @@ -0,0 +1,127 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package logr contains helpers for setting up a Kubernetes object aware logr.Logger. +package logr + +import ( + "reflect" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ logr.LogSink = (*KubeAwareSink)(nil) + +// KubeAwareSink is a logr.LogSink that understands Kubernetes objects. +type KubeAwareSink struct { + sink logr.LogSink +} + +// KubeAware wraps a logr.logger to make it aware of Kubernetes objects. +func KubeAware(logger logr.Logger) logr.Logger { + return logr.New( + &KubeAwareSink{logger.GetSink()}, + ) +} + +// Init implements logr.LogSink. +func (k *KubeAwareSink) Init(info logr.RuntimeInfo) { + k.sink.Init(info) +} + +// Enabled implements logr.LogSink. +func (k *KubeAwareSink) Enabled(level int) bool { + return k.sink.Enabled(level) +} + +// Info implements logr.LogSink. +func (k *KubeAwareSink) Info(level int, msg string, keysAndValues ...interface{}) { + k.sink.Info(level, msg, k.wrapKeyAndValues(keysAndValues)...) +} + +// Error implements logr.LogSink. +func (k *KubeAwareSink) Error(err error, msg string, keysAndValues ...interface{}) { + k.sink.Error(err, msg, k.wrapKeyAndValues(keysAndValues)...) +} + +// WithValues implements logr.LogSink. +func (k *KubeAwareSink) WithValues(keysAndValues ...interface{}) logr.LogSink { + return &KubeAwareSink{ + sink: k.sink.WithValues(k.wrapKeyAndValues(keysAndValues)...), + } +} + +// WithName implements logr.LogSink. +func (k *KubeAwareSink) WithName(name string) logr.LogSink { + return &KubeAwareSink{ + sink: k.sink.WithName(name), + } +} + +// wrapKeyAndValues replaces Kubernetes objects with [kubeObjectWrapper]. +func (k *KubeAwareSink) wrapKeyAndValues(keysAndValues []interface{}) []interface{} { + result := make([]interface{}, len(keysAndValues)) + for i, item := range keysAndValues { + if i%2 == 0 { + // item is key, no need to resolve + result[i] = item + continue + } + + switch val := item.(type) { + case runtime.Object: + result[i] = &kubeObjectWrapper{obj: val} + default: + result[i] = item + } + } + return result +} + +var _ logr.Marshaler = (*kubeObjectWrapper)(nil) + +// kubeObjectWrapper is a wrapper around runtime.Object that implements logr.Marshaler. +type kubeObjectWrapper struct { + obj runtime.Object +} + +// MarshalLog implements logr.Marshaler. +// The implementation mirrors the behavior of kubeObjectWrapper.MarshalLogObject. +func (w *kubeObjectWrapper) MarshalLog() interface{} { + result := make(map[string]string) + + if reflect.ValueOf(w.obj).IsNil() { + return "got nil for runtime.Object" + } + + if gvk := w.obj.GetObjectKind().GroupVersionKind(); gvk.Version != "" { + result["apiVersion"] = gvk.GroupVersion().String() + result["kind"] = gvk.Kind + } + + objMeta, err := meta.Accessor(w.obj) + if err != nil { + return result + } + + if ns := objMeta.GetNamespace(); ns != "" { + result["namespace"] = ns + } + result["name"] = objMeta.GetName() + return result +} diff --git a/pkg/log/zap/zap.go b/pkg/log/zap/zap.go index 3a114667bd..76a9aae1b3 100644 --- a/pkg/log/zap/zap.go +++ b/pkg/log/zap/zap.go @@ -28,6 +28,8 @@ import ( "github.com/go-logr/zapr" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + logrhelper "sigs.k8s.io/controller-runtime/pkg/log/logr" ) // EncoderConfigOption is a function that can modify a `zapcore.EncoderConfig`. @@ -36,11 +38,14 @@ type EncoderConfigOption func(*zapcore.EncoderConfig) // NewEncoderFunc is a function that creates an Encoder using the provided EncoderConfigOptions. type NewEncoderFunc func(...EncoderConfigOption) zapcore.Encoder -// New returns a brand new Logger configured with Opts. It -// uses KubeAwareEncoder which adds Type information and -// Namespace/Name to the log. +// New returns a brand new Logger configured with Opts. +// It uses [logrhelper.KubeAware] to make the logger Kubernetes-aware, +// meaning that for Kubernetes objects, the logger will by default digest only the metadata. func New(opts ...Opts) logr.Logger { - return zapr.NewLogger(NewRaw(opts...)) + zrl := zapr.NewLogger(NewRaw(opts...)) + // Make sure the created logr.logger is Kubernetes-aware. + // See https://github.com/kubernetes-sigs/controller-runtime/issues/1290 for the background. + return logrhelper.KubeAware(zrl) } // Opts allows to manipulate Options. diff --git a/pkg/log/zap/zap_test.go b/pkg/log/zap/zap_test.go index f7fad41f06..ab50acb773 100644 --- a/pkg/log/zap/zap_test.go +++ b/pkg/log/zap/zap_test.go @@ -264,6 +264,59 @@ var _ = Describe("Zap logger setup", func() { outRaw := logOut.Bytes() Expect(string(outRaw)).Should(ContainSubstring("got nil for runtime.Object")) }) + + It("should log a standard namespaced when using logrLogger.WithValues", func() { + name := types.NamespacedName{Name: "some-pod", Namespace: "some-ns"} + logger.WithValues("thing", name).Info("here's a kubernetes object") + + outRaw := logOut.Bytes() + res := map[string]interface{}{} + Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) + + Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + })) + }) + + It("should log a standard Kubernetes objects when using logrLogger.WithValues", func() { + node := &corev1.Node{} + node.Name = "a-node" + node.APIVersion = "v1" + node.Kind = "Node" + logger.WithValues("thing", node).Info("here's a kubernetes object") + + outRaw := logOut.Bytes() + res := map[string]interface{}{} + Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) + + Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + "name": node.Name, + "apiVersion": node.APIVersion, + "kind": node.Kind, + })) + }) + + It("should log a standard unstructured Kubernetes object when using logrLogger.WithValues", func() { + pod := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "a-pod", + "namespace": "a-ns", + }, + }, + } + logger.WithValues("thing", pod).Info("here's a kubernetes object") + + outRaw := logOut.Bytes() + res := map[string]interface{}{} + Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) + + Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + "name": "a-pod", + "namespace": "a-ns", + })) + }) } Context("with logger created using New", func() {