Skip to content

Commit 37142fa

Browse files
committed
Add helper for nicely logging Kubernetes objects
This adds a "wrapper" to a Zap encoder designed to nicely log Kubernetes objects when in non-development mode. When in development mode, we still log the entire object.
1 parent b6eb718 commit 37142fa

File tree

3 files changed

+236
-39
lines changed

3 files changed

+236
-39
lines changed

pkg/runtime/log/kube_helpers.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2018 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 log contains utilities for fetching a new logger
18+
// when one is not already available.
19+
package log
20+
21+
import (
22+
"fmt"
23+
24+
"go.uber.org/zap/buffer"
25+
"go.uber.org/zap/zapcore"
26+
"k8s.io/apimachinery/pkg/api/meta"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/types"
29+
)
30+
31+
// KubeAwareEncoder is a Kubernetes-aware Zap Encoder.
32+
// Instead of trying to force Kubernetes objects to implement
33+
// ObjectMarshaller, we just implement a wrapper around a normal
34+
// ObjectMarshaller that checks for Kubernetes objects.
35+
type KubeAwareEncoder struct {
36+
// Encoder is the zapcore.Encoder that this encoder delegates to
37+
zapcore.Encoder
38+
39+
// Verbose controls whether or not the full object is printed.
40+
// If false, only name, namespace, api version, and kind are printed.
41+
// Otherwise, the full object is logged.
42+
Verbose bool
43+
}
44+
45+
// namespacedNameWrapper is a zapcore.ObjectMarshaler for Kubernetes NamespacedName
46+
type namespacedNameWrapper struct {
47+
types.NamespacedName
48+
}
49+
50+
func (w namespacedNameWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error {
51+
if w.Namespace != "" {
52+
enc.AddString("namespace", w.Namespace)
53+
}
54+
55+
enc.AddString("name", w.Name)
56+
57+
return nil
58+
}
59+
60+
// kubeObjectWrapper is a zapcore.ObjectMarshaler for Kubernetes objects.
61+
type kubeObjectWrapper struct {
62+
obj runtime.Object
63+
}
64+
65+
// MarshalLogObject implements zapcore.ObjectMarshaler
66+
func (w kubeObjectWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error {
67+
// TODO(directxman12): log kind and apiversion if not set explicitly (common case)
68+
// -- needs an a scheme to convert to the GVK.
69+
gvk := w.obj.GetObjectKind().GroupVersionKind()
70+
if gvk.Version != "" {
71+
enc.AddString("apiVersion", gvk.GroupVersion().String())
72+
enc.AddString("kind", gvk.Kind)
73+
}
74+
75+
objMeta, err := meta.Accessor(w.obj)
76+
if err != nil {
77+
return fmt.Errorf("got runtime.Object without object metadata: %v", w.obj)
78+
}
79+
80+
ns := objMeta.GetNamespace()
81+
if ns != "" {
82+
enc.AddString("namespace", ns)
83+
}
84+
enc.AddString("name", objMeta.GetName())
85+
86+
return nil
87+
}
88+
89+
// NB(directxman12): can't just override AddReflected, since the encoder calls AddReflected on itself directly
90+
91+
// Clone implements zapcore.Encoder
92+
func (k *KubeAwareEncoder) Clone() zapcore.Encoder {
93+
return &KubeAwareEncoder{
94+
Encoder: k.Encoder.Clone(),
95+
}
96+
}
97+
98+
// EncodeEntry implements zapcore.Encoder
99+
func (k *KubeAwareEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
100+
if k.Verbose {
101+
// Kubernetes objects implement fmt.Stringer, so if we
102+
// want verbose output, just delegate to that.
103+
return k.Encoder.EncodeEntry(entry, fields)
104+
}
105+
106+
for i, field := range fields {
107+
// intercept stringer fields that happen to be Kubernetes runtime.Object or
108+
// types.NamespacedName values (Kubernetes runtime.Objects commonly
109+
// implement String, apparently).
110+
if field.Type == zapcore.StringerType {
111+
switch val := field.Interface.(type) {
112+
case runtime.Object:
113+
fields[i] = zapcore.Field{
114+
Type: zapcore.ObjectMarshalerType,
115+
Key: field.Key,
116+
Interface: kubeObjectWrapper{obj: val},
117+
}
118+
case types.NamespacedName:
119+
fields[i] = zapcore.Field{
120+
Type: zapcore.ObjectMarshalerType,
121+
Key: field.Key,
122+
Interface: namespacedNameWrapper{NamespacedName: val},
123+
}
124+
}
125+
}
126+
}
127+
128+
return k.Encoder.EncodeEntry(entry, fields)
129+
}

pkg/runtime/log/log.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package log
2020

2121
import (
2222
"io"
23-
"log"
23+
"os"
2424
"time"
2525

2626
"github.com/go-logr/logr"
@@ -34,18 +34,7 @@ import (
3434
// (stacktraces on warnings, no sampling), otherwise a Zap production
3535
// config will be used (stacktraces on errors, sampling).
3636
func ZapLogger(development bool) logr.Logger {
37-
var zapLog *zap.Logger
38-
var err error
39-
if development {
40-
zapLogCfg := zap.NewDevelopmentConfig()
41-
zapLog, err = zapLogCfg.Build(zap.AddCallerSkip(1))
42-
} else {
43-
zapLogCfg := zap.NewProductionConfig()
44-
zapLog, err = zapLogCfg.Build(zap.AddCallerSkip(1))
45-
}
46-
// who watches the watchmen?
47-
fatalIfErr(err, log.Fatalf)
48-
return zapr.NewLogger(zapLog)
37+
return ZapLoggerTo(os.Stderr, development)
4938
}
5039

5140
// ZapLoggerTo returns a new Logger implementation using Zap which logs
@@ -73,17 +62,11 @@ func ZapLoggerTo(destWriter io.Writer, development bool) logr.Logger {
7362
}))
7463
}
7564
opts = append(opts, zap.AddCallerSkip(1), zap.ErrorOutput(sink))
76-
log := zap.New(zapcore.NewCore(enc, sink, lvl))
65+
log := zap.New(zapcore.NewCore(&KubeAwareEncoder{Encoder: enc, Verbose: development}, sink, lvl))
7766
log = log.WithOptions(opts...)
7867
return zapr.NewLogger(log)
7968
}
8069

81-
func fatalIfErr(err error, f func(format string, v ...interface{})) {
82-
if err != nil {
83-
f("unable to construct the logger: %v", err)
84-
}
85-
}
86-
8770
// SetLogger sets a concrete logging implementation for all deferred Loggers.
8871
func SetLogger(l logr.Logger) {
8972
Log.Fulfill(l)

pkg/runtime/log/log_test.go

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ limitations under the License.
1717
package log
1818

1919
import (
20-
"fmt"
20+
"bytes"
21+
"encoding/json"
2122
"io/ioutil"
2223

2324
"github.com/go-logr/logr"
2425
. "github.com/onsi/ginkgo"
2526
. "github.com/onsi/gomega"
27+
kapi "k8s.io/api/core/v1"
28+
"k8s.io/apimachinery/pkg/types"
2629
)
2730

31+
// testStringer is a fmt.Stringer
32+
type testStringer struct{}
33+
34+
func (testStringer) String() string {
35+
return "value"
36+
}
37+
2838
// fakeSyncWriter is a fake zap.SyncerWriter that lets us test if sync was called
2939
type fakeSyncWriter bool
3040

@@ -249,26 +259,101 @@ var _ = Describe("runtime log", func() {
249259
Expect(ZapLoggerTo(ioutil.Discard, true)).NotTo(BeNil())
250260
})
251261
})
252-
})
253262

254-
Describe("fataliferr", func() {
255-
It("should not call the fn if there is not an error", func() {
256-
called := false
257-
fn := func(format string, v ...interface{}) {
258-
called = true
259-
}
260-
fatalIfErr(nil, fn)
261-
Expect(called).To(BeFalse())
262-
})
263+
Context("when logging kubernetes objects", func() {
264+
var logOut *bytes.Buffer
265+
var logger logr.Logger
266+
267+
BeforeEach(func() {
268+
logOut = new(bytes.Buffer)
269+
By("setting up the logger")
270+
// use production settings (false) to get just json output
271+
logger = ZapLoggerTo(logOut, false)
272+
})
273+
274+
It("should log a standard namespaced Kubernetes object name and namespace", func() {
275+
pod := &kapi.Pod{}
276+
pod.Name = "some-pod"
277+
pod.Namespace = "some-ns"
278+
logger.Info("here's a kubernetes object", "thing", pod)
279+
280+
outRaw := logOut.Bytes()
281+
res := map[string]interface{}{}
282+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
283+
284+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
285+
"name": pod.Name,
286+
"namespace": pod.Namespace,
287+
}))
288+
})
289+
290+
It("should work fine with normal stringers", func() {
291+
logger.Info("here's a non-kubernetes stringer", "thing", testStringer{})
292+
outRaw := logOut.Bytes()
293+
res := map[string]interface{}{}
294+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
295+
296+
Expect(res).To(HaveKeyWithValue("thing", "value"))
297+
})
263298

264-
It("should call the fn if there is an error", func() {
265-
called := false
266-
fn := func(format string, v ...interface{}) {
267-
called = true
268-
}
269-
fatalIfErr(fmt.Errorf("error"), fn)
270-
Expect(called).To(BeTrue())
299+
It("should log a standard non-namespaced Kubernetes object name", func() {
300+
node := &kapi.Node{}
301+
node.Name = "some-node"
302+
logger.Info("here's a kubernetes object", "thing", node)
303+
304+
outRaw := logOut.Bytes()
305+
res := map[string]interface{}{}
306+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
307+
308+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
309+
"name": node.Name,
310+
}))
311+
})
312+
313+
It("should log a standard Kubernetes object's kind, if set", func() {
314+
node := &kapi.Node{}
315+
node.Name = "some-node"
316+
node.APIVersion = "v1"
317+
node.Kind = "Node"
318+
logger.Info("here's a kubernetes object", "thing", node)
319+
320+
outRaw := logOut.Bytes()
321+
res := map[string]interface{}{}
322+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
323+
324+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
325+
"name": node.Name,
326+
"apiVersion": "v1",
327+
"kind": "Node",
328+
}))
329+
})
330+
331+
It("should log a standard non-namespaced NamespacedName name", func() {
332+
name := types.NamespacedName{Name: "some-node"}
333+
logger.Info("here's a kubernetes object", "thing", name)
334+
335+
outRaw := logOut.Bytes()
336+
res := map[string]interface{}{}
337+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
338+
339+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
340+
"name": name.Name,
341+
}))
342+
})
343+
344+
It("should log a standard namespaced NamespacedName name and namespace", func() {
345+
name := types.NamespacedName{Name: "some-pod", Namespace: "some-ns"}
346+
logger.Info("here's a kubernetes object", "thing", name)
347+
348+
outRaw := logOut.Bytes()
349+
res := map[string]interface{}{}
350+
Expect(json.Unmarshal(outRaw, &res)).To(Succeed())
351+
352+
Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{
353+
"name": name.Name,
354+
"namespace": name.Namespace,
355+
}))
356+
})
271357
})
272358
})
273-
274359
})

0 commit comments

Comments
 (0)