From 15d8fad76c0d480b97d640b56579856b02192fdd Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 20 Sep 2021 10:17:25 +0200 Subject: [PATCH] LogObject: optional interface for logging objects differently The intention is to use this when the output is structured (like JSON) when the original type would be logged as string. It also has some other use cases. This approach was chosen instead of a full marshaler API as in zapcore.ObjectMarshaler because the implementation is simpler. The overhead for large types is expected to be higher, but it is not certain yet whether this is relevant in practice. If it is, then a marshaler API can still be added later. --- example_marshaler_secret_test.go | 47 ++++++++++++++++++++++ example_marshaler_test.go | 69 ++++++++++++++++++++++++++++++++ funcr/funcr.go | 4 ++ logr.go | 18 +++++++++ 4 files changed, 138 insertions(+) create mode 100644 example_marshaler_secret_test.go create mode 100644 example_marshaler_test.go diff --git a/example_marshaler_secret_test.go b/example_marshaler_secret_test.go new file mode 100644 index 0000000..1e59af3 --- /dev/null +++ b/example_marshaler_secret_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2021 The logr 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_test + +import ( + "github.com/go-logr/logr" +) + +// ComplexObjectRef contains more fields than it wants to get logged. +type ComplexObjectRef struct { + Name string + Namespace string + Secret string +} + +func (ref ComplexObjectRef) MarshalLog() interface{} { + return struct { + Name, Namespace string + }{ + Name: ref.Name, + Namespace: ref.Namespace, + } +} + +var _ logr.Marshaler = ComplexObjectRef{} + +func ExampleMarshaler_secret() { + l := NewStdoutLogger() + secret := ComplexObjectRef{Namespace: "kube-system", Name: "some-secret", Secret: "do-not-log-me"} + l.Info("simplified", "secret", secret) + // Output: + // "level"=0 "msg"="simplified" "secret"={"Name":"some-secret","Namespace":"kube-system"} +} diff --git a/example_marshaler_test.go b/example_marshaler_test.go new file mode 100644 index 0000000..934b574 --- /dev/null +++ b/example_marshaler_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2021 The logr 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_test + +import ( + "fmt" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/funcr" +) + +// NewStdoutLogger returns a logr.Logger that prints to stdout. +func NewStdoutLogger() logr.Logger { + return funcr.New(func(prefix, args string) { + if prefix != "" { + _ = fmt.Sprintf("%s: %s\n", prefix, args) + } else { + fmt.Println(args) + } + }, funcr.Options{}) +} + +// ObjectRef references a Kubernetes object +type ObjectRef struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +func (ref ObjectRef) String() string { + if ref.Namespace != "" { + return ref.Namespace + "/" + ref.Name + } + return ref.Name +} + +func (ref ObjectRef) MarshalLog() interface{} { + // We implement fmt.Stringer for non-structured logging, but we want the + // raw struct when using structured logs. Some logr implementations call + // String if it is present, so we want to convert this struct to something + // that doesn't have that method. + type forLog ObjectRef // methods do not survive type definitions + return forLog(ref) +} + +var _ logr.Marshaler = ObjectRef{} + +func ExampleMarshaler() { + l := NewStdoutLogger() + pod := ObjectRef{Namespace: "kube-system", Name: "some-pod"} + l.Info("as string", "pod", pod.String()) + l.Info("as struct", "pod", pod) + // Output: + // "level"=0 "msg"="as string" "pod"="kube-system/some-pod" + // "level"=0 "msg"="as struct" "pod"={"name":"some-pod","namespace":"kube-system"} +} diff --git a/funcr/funcr.go b/funcr/funcr.go index ff47817..b7acfc0 100644 --- a/funcr/funcr.go +++ b/funcr/funcr.go @@ -170,6 +170,10 @@ const ( func prettyWithFlags(value interface{}, flags uint32) string { // Handling the most common types without reflect is a small perf win. switch v := value.(type) { + case logr.Marshaler: + // Replace the value with what the value wants to get logged. + // That then gets handled below via reflection. + value = v.MarshalLog() case bool: return strconv.FormatBool(v) case string: diff --git a/logr.go b/logr.go index 4e84ab7..3751180 100644 --- a/logr.go +++ b/logr.go @@ -477,3 +477,21 @@ type CallStackHelperLogSink interface { // call site information. GetCallStackHelper() func() } + +// Marshaler is an optional interface that logged values may choose to +// implement. Loggers with structured output, such as JSON, should +// log the object return by the MarshalLog method instead of the +// original value. +type Marshaler interface { + // MarshalLog can be used to: + // - ensure that structs are not logged as strings when the original + // value has a String method: return a different type without a + // String method + // - select which fields of a complex type should get logged: + // return a simpler struct with fewer fields + // - log unexported fields: return a different struct + // with exported fields + // + // It may return any value of any type. + MarshalLog() interface{} +}