Skip to content

⚠️ kube aware logger which could also work with logger.WithValues #1883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions examples/scratch-env/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/scratch-env/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
125 changes: 125 additions & 0 deletions pkg/log/zap/kube_aware_logr_logger_sink.go
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions pkg/log/zap/logr_logger_kube_object_wrapper.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions pkg/log/zap/logr_logger_namespaced_name_wrapper.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 4 additions & 3 deletions pkg/log/zap/zap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 59 additions & 5 deletions pkg/log/zap/zap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down