Skip to content
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

embed funcr, custom With* function example #68

Merged
merged 1 commit into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions funcr/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package funcr_test

import (
"fmt"
"strings"

"github.com/go-logr/logr"
"github.com/go-logr/logr/funcr"
Expand All @@ -34,3 +35,81 @@ func ExampleUnderlier() {
}
// Output: hello world
}

// NewStdoutLogger returns a logr.Logger that prints to stdout.
// It demonstrates how to implement a custom With* function which
// controls whether INFO or ERROR are printed in front of the log
// message.
func NewStdoutLogger() logr.Logger {
l := &stdoutlogger{
Formatter: funcr.NewFormatter(funcr.Options{}),
}
return logr.New(l)
}

type stdoutlogger struct {
funcr.Formatter
logMsgType bool
}

func (l stdoutlogger) WithName(name string) logr.LogSink {
l.Formatter.AddName(name)
return &l
}

func (l stdoutlogger) WithValues(kvList ...interface{}) logr.LogSink {
l.Formatter.AddValues(kvList)
return &l
}

func (l stdoutlogger) WithCallDepth(depth int) logr.LogSink {
l.Formatter.AddCallDepth(depth)
return &l
}

func (l stdoutlogger) Info(level int, msg string, kvList ...interface{}) {
prefix, args := l.FormatInfo(level, msg, kvList)
l.write("INFO", prefix, args)
}

func (l stdoutlogger) Error(err error, msg string, kvList ...interface{}) {
prefix, args := l.FormatError(err, msg, kvList)
l.write("ERROR", prefix, args)
}

func (l stdoutlogger) write(msgType, prefix, args string) {
var parts []string
if l.logMsgType {
parts = append(parts, msgType)
}
if prefix != "" {
parts = append(parts, prefix)
}
parts = append(parts, args)
fmt.Println(strings.Join(parts, ": "))
}

// WithLogMsgType returns a copy of the logger with new settings for
// logging the message type. It returns the original logger if the
// underlying LogSink is not a stdoutlogger.
func WithLogMsgType(log logr.Logger, logMsgType bool) logr.Logger {
if l, ok := log.GetSink().(*stdoutlogger); ok {
clone := *l
clone.logMsgType = logMsgType
log = log.WithSink(&clone)
}
return log
}

// Assert conformance to the interfaces.
var _ logr.LogSink = &stdoutlogger{}
var _ logr.CallDepthLogSink = &stdoutlogger{}

func ExampleFormatter() {
l := NewStdoutLogger()
l.Info("no message type")
WithLogMsgType(l, true).Info("with message type")
// Output:
// "level"=0 "msg"="no message type"
// INFO: "level"=0 "msg"="with message type"
}
173 changes: 102 additions & 71 deletions funcr/funcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

// Package funcr implements github.com/go-logr/logr.Logger in terms of
// an arbitrary "write" function. This will not call String or
// Package funcr implements formatting of structured log messages and
// optionally captures the call site. This will not call String or
// Error methods on values.
//
// The simplest way to use it is via its implementation of a
// github.com/go-logr/logr.LogSink with output through an arbitrary
// "write" function. Alternatively, funcr can also be embedded inside
// a custom LogSink implementation. This is useful when the LogSink
// needs to implement additional methods.
package funcr

import (
Expand Down Expand Up @@ -47,20 +53,11 @@ type Underlier interface {

func newSink(fn func(prefix, args string), opts Options) logr.LogSink {
l := &fnlogger{
prefix: "",
values: nil,
depth: 0,
write: fn,
logCaller: opts.LogCaller,
logTimestamp: opts.LogTimestamp,
verbosity: opts.Verbosity,
helper: opts.Helper,
}
if l.helper == nil {
// We have to have a valid function for GetCallStackHelper, so
// we might as well just cover the nil case once here.
l.helper = func() {}
Formatter: NewFormatter(opts),
write: fn,
}
// For skipping fnlogger.Info and fnlogger.Error.
l.Formatter.AddCallDepth(1)
return l
}

Expand All @@ -77,10 +74,6 @@ type Options struct {
// Verbosity tells funcr which V logs to be write. Higher values enable
// more logs.
Verbosity int

// Helper is an optional function that funcr will call to mark its own
// stack frames as helper functions.
Helper func()
}

// MessageClass indicates which category or categories of messages to consider.
Expand All @@ -95,22 +88,46 @@ const (

const timestampFmt = "2006-01-02 15:04:05.000000"

// fnlogger inherits some of its LogSink implementation from Formatter
// and just needs to add some glue code.
type fnlogger struct {
prefix string
values []interface{}
depth int
write func(prefix, args string)
helper func()
logCaller MessageClass
logTimestamp bool
verbosity int
Formatter
write func(prefix, args string)
}

func (l fnlogger) WithName(name string) logr.LogSink {
l.Formatter.AddName(name)
return &l
}

func (l fnlogger) WithValues(kvList ...interface{}) logr.LogSink {
l.Formatter.AddValues(kvList)
return &l
}

func (l fnlogger) WithCallDepth(depth int) logr.LogSink {
l.Formatter.AddCallDepth(depth)
return &l
}

func (l fnlogger) Info(level int, msg string, kvList ...interface{}) {
prefix, args := l.FormatInfo(level, msg, kvList)
l.write(prefix, args)
}

func (l fnlogger) Error(err error, msg string, kvList ...interface{}) {
prefix, args := l.FormatError(err, msg, kvList)
l.write(prefix, args)
}

func (l fnlogger) GetUnderlying() func(prefix, args string) {
return l.write
}

// Assert conformance to the interfaces.
var _ logr.LogSink = &fnlogger{}
var _ logr.CallDepthLogSink = &fnlogger{}
var _ Underlier = &fnlogger{}
var _ logr.CallStackHelperLogSink = &fnlogger{}

func flatten(kvList ...interface{}) string {
if len(kvList)%2 != 0 {
Expand Down Expand Up @@ -276,88 +293,102 @@ type callerID struct {
Line int `json:"line"`
}

func (l fnlogger) caller() callerID {
// NewFormatter constructs a Formatter.
func NewFormatter(opts Options) Formatter {
f := Formatter{
prefix: "",
values: nil,
depth: 0,
logCaller: opts.LogCaller,
logTimestamp: opts.LogTimestamp,
verbosity: opts.Verbosity,
}
return f
}

// Formatter is an opaque struct which can be embedded in a LogSink
// implementation. It should be constructed with NewFormatter. Some of
// its methods directly implement logr.LogSink.
type Formatter struct {
prefix string
values []interface{}
depth int
logCaller MessageClass
logTimestamp bool
verbosity int
}

func (f Formatter) caller() callerID {
// +1 for this frame, +1 for Info/Error.
_, file, line, ok := runtime.Caller(l.depth + 2)
_, file, line, ok := runtime.Caller(f.depth + 2)
if !ok {
return callerID{"<unknown>", 0}
}
return callerID{filepath.Base(file), line}
}

// Note that this receiver is a pointer, so depth can be saved.
func (l *fnlogger) Init(info logr.RuntimeInfo) {
l.depth += info.CallDepth
func (f *Formatter) Init(info logr.RuntimeInfo) {
f.depth += info.CallDepth
}

func (l fnlogger) Enabled(level int) bool {
return level <= l.verbosity
func (f Formatter) Enabled(level int) bool {
return level <= f.verbosity
}

func (l fnlogger) Info(level int, msg string, kvList ...interface{}) {
// FormatInfo flattens an Info log message into strings.
// The prefix will be empty when no names were set.
func (f Formatter) FormatInfo(level int, msg string, kvList []interface{}) (prefix, argsStr string) {
args := make([]interface{}, 0, 64) // using a constant here impacts perf
if l.logTimestamp {
if f.logTimestamp {
args = append(args, "ts", time.Now().Format(timestampFmt))
}
if l.logCaller == All || l.logCaller == Info {
args = append(args, "caller", l.caller())
if f.logCaller == All || f.logCaller == Info {
args = append(args, "caller", f.caller())
}
args = append(args, "level", level, "msg", msg)
args = append(args, l.values...)
args = append(args, f.values...)
args = append(args, kvList...)
argsStr := flatten(args...)
l.helper()
l.write(l.prefix, argsStr)
return f.prefix, flatten(args...)
}

func (l fnlogger) Error(err error, msg string, kvList ...interface{}) {
// FormatInfo flattens an Error log message into strings.
// The prefix will be empty when no names were set.
func (f Formatter) FormatError(err error, msg string, kvList []interface{}) (prefix, argsStr string) {
args := make([]interface{}, 0, 64) // using a constant here impacts perf
if l.logTimestamp {
if f.logTimestamp {
args = append(args, "ts", time.Now().Format(timestampFmt))
}
if l.logCaller == All || l.logCaller == Error {
args = append(args, "caller", l.caller())
if f.logCaller == All || f.logCaller == Error {
args = append(args, "caller", f.caller())
}
args = append(args, "msg", msg)
var loggableErr interface{}
if err != nil {
loggableErr = err.Error()
}
args = append(args, "error", loggableErr)
args = append(args, l.values...)
args = append(args, f.values...)
args = append(args, kvList...)
argsStr := flatten(args...)
l.helper()
l.write(l.prefix, argsStr)
return f.prefix, flatten(args...)
}

// WithName returns a new Logger with the specified name appended. funcr
// AddName appends the specified name. funcr
// uses '/' characters to separate name elements. Callers should not pass '/'
// in the provided name string, but this library does not actually enforce that.
func (l fnlogger) WithName(name string) logr.LogSink {
if len(l.prefix) > 0 {
l.prefix += "/"
func (f *Formatter) AddName(name string) {
if len(f.prefix) > 0 {
f.prefix += "/"
}
l.prefix += name
return &l
f.prefix += name
}

func (l fnlogger) WithValues(kvList ...interface{}) logr.LogSink {
func (f *Formatter) AddValues(kvList []interface{}) {
// Three slice args forces a copy.
n := len(l.values)
l.values = append(l.values[:n:n], kvList...)
return &l
}

func (l fnlogger) WithCallDepth(depth int) logr.LogSink {
l.depth += depth
return &l
}

func (l fnlogger) GetUnderlying() func(prefix, args string) {
return l.write
n := len(f.values)
f.values = append(f.values[:n:n], kvList...)
}

func (l fnlogger) GetCallStackHelper() func() {
return l.helper
func (f *Formatter) AddCallDepth(depth int) {
f.depth += depth
}
Loading