diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 010adc9379..72ccf80ab6 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -39,6 +39,7 @@ require ( github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index b5246d60a2..5b2239ea14 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -103,6 +103,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/go.mod b/go.mod index d2e4b5ddea..8f9ca95a20 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/onsi/gomega v1.35.1 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 + go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect diff --git a/go.sum b/go.sum index a393c17856..c22da4ddb9 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+ go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/pkg/log/zap/kube_aware_logr_logger_sink.go b/pkg/log/zap/kube_aware_logr_logger_sink.go new file mode 100644 index 0000000000..df5365c505 --- /dev/null +++ b/pkg/log/zap/kube_aware_logr_logger_sink.go @@ -0,0 +1,125 @@ +/* +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 zap + +import ( + "github.com/go-logr/logr" + "go.uber.org/atomic" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +var _ logr.LogSink = (*KubeAwareLogSink)(nil) + +// KubeAwareLogSink is a Kubernetes-aware logr.LogSink. +// zapcore.ObjectMarshaler would be bypassed when using zapr and WithValues. +// It would use a wrapper implements logr.Marshaler instead of using origin Kubernetes objects. +type KubeAwareLogSink struct { + sink logr.LogSink + kubeAwareEnabled *atomic.Bool +} + +// NewKubeAwareLogrLogger return the wrapper with existed logr.Logger. +// logger is the backend logger. +// kubeAwareEnabled is the flag to enable kube aware logging. +func NewKubeAwareLogrLogger(logger logr.Logger, kubeAwareEnabled bool) logr.Logger { + return logr.New(NewKubeAwareLogSink(logger.GetSink(), kubeAwareEnabled)) +} + +// NewKubeAwareLogSink return the wrapper with existed logr.LogSink. +// sink is the backend logr.LogSink. +// kubeAwareEnabled is the flag to enable kube aware logging. +func NewKubeAwareLogSink(logSink logr.LogSink, kubeAwareEnabled bool) *KubeAwareLogSink { + return &KubeAwareLogSink{sink: logSink, kubeAwareEnabled: atomic.NewBool(kubeAwareEnabled)} +} + +// Init implements logr.LogSink. +func (k *KubeAwareLogSink) Init(info logr.RuntimeInfo) { + k.sink.Init(info) +} + +// Enabled implements logr.LogSink. +func (k *KubeAwareLogSink) Enabled(level int) bool { + return k.sink.Enabled(level) +} + +// Info implements logr.LogSink. +func (k *KubeAwareLogSink) Info(level int, msg string, keysAndValues ...interface{}) { + if !k.KubeAwareEnabled() { + k.sink.Info(level, msg, keysAndValues...) + return + } + + k.sink.Info(level, msg, k.wrapKeyAndValues(keysAndValues)...) +} + +// Error implements logr.LogSink. +func (k *KubeAwareLogSink) Error(err error, msg string, keysAndValues ...interface{}) { + if !k.KubeAwareEnabled() { + k.sink.Error(err, msg, keysAndValues...) + return + } + k.sink.Error(err, msg, k.wrapKeyAndValues(keysAndValues)...) +} + +// WithValues implements logr.LogSink. +func (k *KubeAwareLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { + return &KubeAwareLogSink{ + kubeAwareEnabled: k.kubeAwareEnabled, + sink: k.sink.WithValues(k.wrapKeyAndValues(keysAndValues)...), + } +} + +// WithName implements logr.LogSink. +func (k *KubeAwareLogSink) WithName(name string) logr.LogSink { + return &KubeAwareLogSink{ + kubeAwareEnabled: k.kubeAwareEnabled, + sink: k.sink.WithName(name), + } +} + +// KubeAwareEnabled return kube aware logging is enabled or not. +func (k *KubeAwareLogSink) KubeAwareEnabled() bool { + return k.kubeAwareEnabled.Load() +} + +// SetKubeAwareEnabled could update the kube aware logging flag. +func (k *KubeAwareLogSink) SetKubeAwareEnabled(enabled bool) { + k.kubeAwareEnabled.Store(enabled) +} + +// wrapKeyAndValues would replace the kubernetes objects with wrappers. +func (k *KubeAwareLogSink) 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] = &logrLoggerKubeObjectWrapper{obj: val} + case types.NamespacedName: + result[i] = &logrLoggerNamespacedNameWrapper{NamespacedName: val} + default: + result[i] = item + } + } + return result +} diff --git a/pkg/log/zap/logr_logger_kube_object_wrapper.go b/pkg/log/zap/logr_logger_kube_object_wrapper.go new file mode 100644 index 0000000000..a7cdbd9ed3 --- /dev/null +++ b/pkg/log/zap/logr_logger_kube_object_wrapper.go @@ -0,0 +1,57 @@ +/* +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 zap + +import ( + "reflect" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ logr.Marshaler = (*logrLoggerKubeObjectWrapper)(nil) + +type logrLoggerKubeObjectWrapper struct { + obj runtime.Object +} + +func (w *logrLoggerKubeObjectWrapper) MarshalLog() interface{} { + result := make(map[string]string) + + if reflect.ValueOf(w.obj).IsNil() { + // keep same behavior with kubeObjectWrapper.MarshalLogObject + 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 { + // best effort, noop + return result + } + + if ns := objMeta.GetNamespace(); ns != "" { + result["namespace"] = ns + } + result["name"] = objMeta.GetName() + return result +} diff --git a/pkg/log/zap/logr_logger_namespaced_name_wrapper.go b/pkg/log/zap/logr_logger_namespaced_name_wrapper.go new file mode 100644 index 0000000000..3742bf9e2b --- /dev/null +++ b/pkg/log/zap/logr_logger_namespaced_name_wrapper.go @@ -0,0 +1,37 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package zap + +import ( + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/types" +) + +var _ logr.Marshaler = (*logrLoggerNamespacedNameWrapper)(nil) + +type logrLoggerNamespacedNameWrapper struct { + types.NamespacedName +} + +func (w *logrLoggerNamespacedNameWrapper) MarshalLog() interface{} { + result := make(map[string]string) + if w.Namespace != "" { + result["namespace"] = w.Namespace + } + result["name"] = w.Name + return result +} diff --git a/pkg/log/zap/zap.go b/pkg/log/zap/zap.go index 3a114667bd..c1338ed263 100644 --- a/pkg/log/zap/zap.go +++ b/pkg/log/zap/zap.go @@ -37,10 +37,11 @@ type EncoderConfigOption func(*zapcore.EncoderConfig) 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. +// uses KubeAwareLogger/KubeAwareEncoder which adds Type +// information and Namespace/Name to the log. func New(opts ...Opts) logr.Logger { - return zapr.NewLogger(NewRaw(opts...)) + zaprLogger := zapr.NewLogger(NewRaw(opts...)) + return NewKubeAwareLogrLogger(zaprLogger, true) } // Opts allows to manipulate Options. diff --git a/pkg/log/zap/zap_test.go b/pkg/log/zap/zap_test.go index f7fad41f06..74ffe6c7d8 100644 --- a/pkg/log/zap/zap_test.go +++ b/pkg/log/zap/zap_test.go @@ -146,6 +146,8 @@ var _ = Describe("Zap options setup", func() { }) }) +const kindNode = "Node" + var _ = Describe("Zap logger setup", func() { Context("when logging kubernetes objects", func() { var logOut *bytes.Buffer @@ -179,7 +181,7 @@ var _ = Describe("Zap logger setup", func() { It("should log a standard non-namespaced Kubernetes object name", func() { node := &corev1.Node{} - node.Name = "some-node" + node.Name = "some-node-1" logger.Info("here's a kubernetes object", "thing", node) outRaw := logOut.Bytes() @@ -193,9 +195,9 @@ var _ = Describe("Zap logger setup", func() { It("should log a standard Kubernetes object's kind, if set", func() { node := &corev1.Node{} - node.Name = "some-node" + node.Name = "some-node-2" node.APIVersion = "v1" - node.Kind = "Node" + node.Kind = kindNode logger.Info("here's a kubernetes object", "thing", node) outRaw := logOut.Bytes() @@ -205,12 +207,12 @@ var _ = Describe("Zap logger setup", func() { Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ "name": node.Name, "apiVersion": "v1", - "kind": "Node", + "kind": kindNode, })) }) It("should log a standard non-namespaced NamespacedName name", func() { - name := types.NamespacedName{Name: "some-node"} + name := types.NamespacedName{Name: "some-node-3"} logger.Info("here's a kubernetes object", "thing", name) outRaw := logOut.Bytes() @@ -264,6 +266,58 @@ 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 = "some-node" + node.APIVersion = "v1" + node.Kind = kindNode + 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": "v1", + "kind": kindNode, + })) + }) + + 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": "some-pod", + "namespace": "some-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": "some-pod", + "namespace": "some-ns", + })) + }) } Context("with logger created using New", func() {