Skip to content

Commit 8c6043e

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 8f98fb7 commit 8c6043e

File tree

2 files changed

+161
-17
lines changed

2 files changed

+161
-17
lines changed

pkg/runtime/log/log.go

+81-16
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,26 @@ limitations under the License.
1919
package log
2020

2121
import (
22+
"fmt"
2223
"io"
23-
"log"
24+
"os"
2425
"time"
2526

2627
"github.com/go-logr/logr"
2728
"github.com/go-logr/zapr"
2829
"go.uber.org/zap"
30+
"go.uber.org/zap/buffer"
2931
"go.uber.org/zap/zapcore"
32+
"k8s.io/apimachinery/pkg/api/meta"
33+
"k8s.io/apimachinery/pkg/runtime"
3034
)
3135

3236
// ZapLogger is a Logger implementation.
3337
// If development is true, a Zap development config will be used
3438
// (stacktraces on warnings, no sampling), otherwise a Zap production
3539
// config will be used (stacktraces on errors, sampling).
3640
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)
41+
return ZapLoggerTo(os.Stderr, development)
4942
}
5043

5144
// ZapLoggerTo returns a new Logger implementation using Zap which logs
@@ -73,15 +66,87 @@ func ZapLoggerTo(destWriter io.Writer, development bool) logr.Logger {
7366
}))
7467
}
7568
opts = append(opts, zap.AddCallerSkip(1), zap.ErrorOutput(sink))
76-
log := zap.New(zapcore.NewCore(enc, sink, lvl))
69+
log := zap.New(zapcore.NewCore(&KubeAwareEncoder{Encoder: enc, Verbose: development}, sink, lvl))
7770
log = log.WithOptions(opts...)
7871
return zapr.NewLogger(log)
7972
}
8073

81-
func fatalIfErr(err error, f func(format string, v ...interface{})) {
74+
// KubeAwareEncoder is a Kubernetes-aware Zap Encoder.
75+
// Instead of trying to force Kubernetes objects to implement
76+
// ObjectMarshaller, we just implement a wrapper around a normal
77+
// ObjectMarshaller that checks for Kubernetes objects.
78+
type KubeAwareEncoder struct {
79+
// Encoder is the zapcore.Encoder that this encoder delegates to
80+
zapcore.Encoder
81+
82+
// Verbose controls whether or not the full object is printed.
83+
// If false, only name, namespace, api version, and kind are printed.
84+
// Otherwise, the full object is logged.
85+
Verbose bool
86+
}
87+
88+
// kubeObjectWrapper is a zapcore.ObjectMarshaller for Kubernetes objects.
89+
type kubeObjectWrapper struct {
90+
obj runtime.Object
91+
key string
92+
}
93+
94+
func (w kubeObjectWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error {
95+
// TODO(directxman12): log kind and apiversion if not set explicitly (common case)
96+
// -- needs an a scheme to convert to the GVK.
97+
gvk := w.obj.GetObjectKind().GroupVersionKind()
98+
if gvk.Version != "" {
99+
enc.AddString("apiVersion", gvk.GroupVersion().String())
100+
enc.AddString("kind", gvk.Kind)
101+
}
102+
103+
objMeta, err := meta.Accessor(w.obj)
82104
if err != nil {
83-
f("unable to construct the logger: %v", err)
105+
return fmt.Errorf("got runtime.Object without object metadata: %v", w.obj)
106+
}
107+
108+
ns := objMeta.GetNamespace()
109+
if ns != "" {
110+
enc.AddString("namespace", ns)
84111
}
112+
enc.AddString("name", objMeta.GetName())
113+
114+
return nil
115+
}
116+
117+
// NB(directxman12): can't just override AddReflected, since the encoder calls AddReflected on itself directly
118+
119+
func (k *KubeAwareEncoder) Clone() zapcore.Encoder {
120+
return &KubeAwareEncoder{
121+
Encoder: k.Encoder.Clone(),
122+
}
123+
}
124+
125+
func (k *KubeAwareEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
126+
if k.Verbose {
127+
// Kubernetes objects implement fmt.Stringer, so if we
128+
// want verbose output, just delegate to that.
129+
return k.Encoder.EncodeEntry(entry, fields)
130+
}
131+
132+
for i, field := range fields {
133+
// intercept stringer fields that happen to be Kubernetes runtime.Object values
134+
// (Kubernetes runtime.Objects commonly implement String, apparently)
135+
if field.Type == zapcore.StringerType {
136+
obj, isObj := field.Interface.(runtime.Object)
137+
if !isObj {
138+
continue
139+
}
140+
// TODO(directxman12): is it more performant to do extract the gvk, name, and namespace lazily?
141+
fields[i] = zapcore.Field{
142+
Type: zapcore.ObjectMarshalerType,
143+
Key: field.Key,
144+
Interface: kubeObjectWrapper{obj: obj},
145+
}
146+
}
147+
}
148+
149+
return k.Encoder.EncodeEntry(entry, fields)
85150
}
86151

87152
// SetLogger sets a concrete logging implementation for all deferred Loggers.

pkg/runtime/log/log_test.go

+80-1
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ limitations under the License.
1717
package log
1818

1919
import (
20+
"bytes"
21+
"encoding/json"
2022
"fmt"
2123
"io/ioutil"
2224

2325
"github.com/go-logr/logr"
2426
. "github.com/onsi/ginkgo"
2527
. "github.com/onsi/gomega"
28+
kapi "k8s.io/api/core/v1"
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,6 +259,76 @@ var _ = Describe("runtime log", func() {
249259
Expect(ZapLoggerTo(ioutil.Discard, true)).NotTo(BeNil())
250260
})
251261
})
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+
})
298+
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+
})
252332
})
253333

254334
Describe("fataliferr", func() {
@@ -270,5 +350,4 @@ var _ = Describe("runtime log", func() {
270350
Expect(called).To(BeTrue())
271351
})
272352
})
273-
274353
})

0 commit comments

Comments
 (0)