Skip to content

Commit

Permalink
LogObject: optional interface for logging objects differently
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pohly committed Sep 27, 2021
1 parent 59f2313 commit 15d8fad
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 0 deletions.
47 changes: 47 additions & 0 deletions example_marshaler_secret_test.go
Original file line number Diff line number Diff line change
@@ -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"}
}
69 changes: 69 additions & 0 deletions example_marshaler_test.go
Original file line number Diff line number Diff line change
@@ -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"}
}
4 changes: 4 additions & 0 deletions funcr/funcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions logr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}

0 comments on commit 15d8fad

Please sign in to comment.