// Package clog implements github.com/go-logr/logr.Logger for CLIs. package clog import ( "bytes" "encoding" "fmt" "io" "reflect" "sort" "strconv" "strings" "text/tabwriter" "github.com/go-logr/logr" "github.com/wojas/genericr" ) // MessageClass indicates which category or categories of messages to consider. type MessageClass int const ( // None ignores all message classes. None MessageClass = iota // All considers all message classes. All // Info only considers info messages. Info // Error only considers error messages. Error ) // New returns CLI friendly logr.Logger implementation. func New(opts ...Option) logr.Logger { return logr.New(newSink(opts...)) } // newSink constructs the logging backend based on genericr func newSink(opts ...Option) logr.LogSink { o := makeOptions(opts...) l := &clog{ logCaller: o.logCaller, out: o.dest, tabWidth: 2, // TODO: allow configuring tabwidth } g := genericr.New(l.logFn). WithVerbosity(o.lvl) if o.logCaller != None { g = g.WithCaller(true) } return g } type clog struct { logCaller MessageClass out io.Writer tabWidth int // number of spaces to use for a single indention level } func (l *clog) logFn(e genericr.Entry) { log := "" if e.Error != nil { log += "[error] " } if e.Name != "" { log += strings.Join(e.NameParts, ":") + ": " } log += l.caller(e) log += e.Message multilineVals := "" for i := 0; i < len(e.Fields); i += 2 { k, ok := e.Fields[i].(string) if !ok { k = fmt.Sprintf("!(%#v)", e.Fields[i]) } var v any if i+1 < len(e.Fields) { v = e.Fields[i+1] } valstr := l.pretty(v) // Don't emit empty key-value pairs. if valstr != "" { // If value is multi-line, it will start with newline. Put k on the next // line, so we end up with something like: // // message key=stringValue key=intValue // arrayKey= // value 1 // value 2 if strings.HasPrefix(valstr, "\n") { // Store multi-line values separately so they can be appended at the // end. multilineVals += "\n" + indent(l.tabWidth, 1) + k + "=" + valstr continue } log += " " + k + "=" + valstr } } // Append multi-line values at the end if multilineVals != "" { log += multilineVals } // Print error on its own line after rest of log values if e.Error != nil { log += "\n" + indent(l.tabWidth, 2) + "err=" + l.pretty(e.Error) } fmt.Fprintln(l.out, log) } func (l *clog) caller(e genericr.Entry) string { switch l.logCaller { case None: return "" case All: return fmtCaller(e) case Info: if e.Error != nil { return "" } return fmtCaller(e) case Error: if e.Error == nil { return "" } return fmtCaller(e) default: return "" } } // formats callers: "" func fmtCaller(e genericr.Entry) string { return "<" + e.Caller.File + ":" + strconv.Itoa(e.Caller.Line) + "> " } // pretty controls the formatting of arbitrary log values into strings that // we can print. some types of values (eg maps and slices) are rendered as a // multiline string. // // when indenting various sections of multiline values, tabs are converted to // spaces for portability. '\t' character should only be used via a tabwriter // for aligning arbitrary key value pairs, because the tabwriter package // converts to spaces under the covers. // // TODO(aw185176): This is still very limited, it needs to handle more complex data types // //nolint:gocyclo // Breaking down this function would reduce clarity and increase complexity func (l *clog) pretty(v any) (ret string) { // Recover from panics if we end up calling a faulty interface implementation defer func() { if r := recover(); r != nil { ret = fmt.Sprintf("", r) } }() // Handle types that take full control of logging. if m, ok := v.(logr.Marshaler); ok { // Replace the value with what the type wants to get logged. // That then gets handled below via reflection. v = m.MarshalLog() } // Handle types that want to format themselves. switch value := v.(type) { case fmt.Stringer: v = value.String() case error: v = value.Error() } // Try handling the most common types without reflect switch v := v.(type) { case bool: return strconv.FormatBool(v) case string: return prettyString(v) case int: return strconv.FormatInt(int64(v), 10) case int8: return strconv.FormatInt(int64(v), 10) case int16: return strconv.FormatInt(int64(v), 10) case int32: return strconv.FormatInt(int64(v), 10) case int64: return strconv.FormatInt(v, 10) case uint: return strconv.FormatUint(uint64(v), 10) case uint8: return strconv.FormatUint(uint64(v), 10) case uint16: return strconv.FormatUint(uint64(v), 10) case uint32: return strconv.FormatUint(uint64(v), 10) case uint64: return strconv.FormatUint(v, 10) case uintptr: return strconv.FormatUint(uint64(v), 10) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case complex64: return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"` case complex128: return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"` } t := reflect.TypeOf(v) if t == nil { return "null" } val := reflect.ValueOf(v) // See if we were able to find simple type via reflection switch t.Kind() { case reflect.Bool: return strconv.FormatBool(val.Bool()) case reflect.String: return prettyString(val.String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(val.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(val.Uint(), 10) case reflect.Float32: return strconv.FormatFloat(val.Float(), 'f', -1, 32) case reflect.Float64: return strconv.FormatFloat(val.Float(), 'f', -1, 64) } // By waiting until after we check simple types we can defer initializing // buffer until we know we need it buf := bytes.NewBuffer(make([]byte, 0, 256)) switch t.Kind() { case reflect.Slice, reflect.Array: if val.Len() == 0 { return "[]" } buf.WriteByte('\n') for i := 0; i < val.Len(); i++ { buf.WriteString(indent(l.tabWidth, 2)) buf.WriteString(l.pretty(val.Index(i).Interface())) // Don't add trailing newline if i < val.Len()-1 { buf.WriteByte('\n') } } return buf.String() case reflect.Map: if val.Len() == 0 { return "{}" } // Keys must be sorted to give reliable map order keys := make([]string, 0, val.Len()) vals := make(map[string]string, val.Len()) buf.WriteByte('\n') it := val.MapRange() i := 0 for it.Next() { valstr := l.pretty(it.Value().Interface()) // Don't log empty map entries if valstr == "" { i++ continue } keystr := "" // If the key supports TextMarshaler, use it if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok { txt, err := m.MarshalText() if err != nil { keystr = fmt.Sprintf("", err.Error()) } else { keystr = string(txt) } keystr = prettyString(keystr) } else { // Otherwise pretty() should do well enough keystr = l.pretty(it.Key().Interface()) } keys = append(keys, keystr) vals[keystr] = valstr i++ } tw := tabwriter.NewWriter(buf, 2, l.tabWidth, 2, ' ', 0) sort.Strings(keys) for i, k := range keys { fmt.Fprintf(tw, "%s%s\t%s", indent(l.tabWidth, 2), k, vals[k]) // Don't add trailing newline if i < len(keys)-1 { fmt.Fprint(tw, "\n") } } tw.Flush() return buf.String() } return fmt.Sprintf("%+v", v) } func prettyString(s string) string { // Avoid escaping (which does allocations) if we can. if needsEscape(s) { return strconv.Quote(s) } return s } // needsEscape determines whether the input string needs to be escaped or not, // without doing any allocations. func needsEscape(s string) bool { for _, r := range s { if !strconv.IsPrint(r) || r == '\\' || r == '"' { return true } } return false } // indent produces a string of spaces representing t tab width * n tabs, because // `\t` can be interpreted differently on different machines. func indent(t int, n int) string { str := "" for i := 0; i < n; i++ { for ii := 0; ii < t; ii++ { str += " " } } return str }