...

Source file src/edge-infra.dev/pkg/lib/cli/clog/clog.go

Documentation: edge-infra.dev/pkg/lib/cli/clog

     1  // Package clog implements github.com/go-logr/logr.Logger for CLIs.
     2  package clog
     3  
     4  import (
     5  	"bytes"
     6  	"encoding"
     7  	"fmt"
     8  	"io"
     9  	"reflect"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/tabwriter"
    14  
    15  	"github.com/go-logr/logr"
    16  	"github.com/wojas/genericr"
    17  )
    18  
    19  // MessageClass indicates which category or categories of messages to consider.
    20  type MessageClass int
    21  
    22  const (
    23  	// None ignores all message classes.
    24  	None MessageClass = iota
    25  	// All considers all message classes.
    26  	All
    27  	// Info only considers info messages.
    28  	Info
    29  	// Error only considers error messages.
    30  	Error
    31  )
    32  
    33  // New returns CLI friendly logr.Logger implementation.
    34  func New(opts ...Option) logr.Logger {
    35  	return logr.New(newSink(opts...))
    36  }
    37  
    38  // newSink constructs the logging backend based on genericr
    39  func newSink(opts ...Option) logr.LogSink {
    40  	o := makeOptions(opts...)
    41  
    42  	l := &clog{
    43  		logCaller: o.logCaller,
    44  		out:       o.dest,
    45  		tabWidth:  2, // TODO: allow configuring tabwidth
    46  	}
    47  
    48  	g := genericr.New(l.logFn).
    49  		WithVerbosity(o.lvl)
    50  
    51  	if o.logCaller != None {
    52  		g = g.WithCaller(true)
    53  	}
    54  
    55  	return g
    56  }
    57  
    58  type clog struct {
    59  	logCaller MessageClass
    60  	out       io.Writer
    61  	tabWidth  int // number of spaces to use for a single indention level
    62  }
    63  
    64  func (l *clog) logFn(e genericr.Entry) {
    65  	log := ""
    66  	if e.Error != nil {
    67  		log += "[error] "
    68  	}
    69  	if e.Name != "" {
    70  		log += strings.Join(e.NameParts, ":") + ": "
    71  	}
    72  	log += l.caller(e)
    73  
    74  	log += e.Message
    75  	multilineVals := ""
    76  	for i := 0; i < len(e.Fields); i += 2 {
    77  		k, ok := e.Fields[i].(string)
    78  		if !ok {
    79  			k = fmt.Sprintf("!(%#v)", e.Fields[i])
    80  		}
    81  
    82  		var v any
    83  		if i+1 < len(e.Fields) {
    84  			v = e.Fields[i+1]
    85  		}
    86  		valstr := l.pretty(v)
    87  		// Don't emit empty key-value pairs.
    88  		if valstr != "" {
    89  			// If value is multi-line, it will start with newline. Put k on the next
    90  			// line, so we end up with something like:
    91  			//
    92  			// message key=stringValue key=intValue
    93  			// 	arrayKey=
    94  			//	 value 1
    95  			// 	 value 2
    96  			if strings.HasPrefix(valstr, "\n") {
    97  				// Store multi-line values separately so they can be appended at the
    98  				// end.
    99  				multilineVals += "\n" + indent(l.tabWidth, 1) + k + "=" + valstr
   100  				continue
   101  			}
   102  			log += " " + k + "=" + valstr
   103  		}
   104  	}
   105  	// Append multi-line values at the end
   106  	if multilineVals != "" {
   107  		log += multilineVals
   108  	}
   109  
   110  	// Print error on its own line after rest of log values
   111  	if e.Error != nil {
   112  		log += "\n" + indent(l.tabWidth, 2) + "err=" + l.pretty(e.Error)
   113  	}
   114  
   115  	fmt.Fprintln(l.out, log)
   116  }
   117  
   118  func (l *clog) caller(e genericr.Entry) string {
   119  	switch l.logCaller {
   120  	case None:
   121  		return ""
   122  	case All:
   123  		return fmtCaller(e)
   124  	case Info:
   125  		if e.Error != nil {
   126  			return ""
   127  		}
   128  		return fmtCaller(e)
   129  	case Error:
   130  		if e.Error == nil {
   131  			return ""
   132  		}
   133  		return fmtCaller(e)
   134  	default:
   135  		return ""
   136  	}
   137  }
   138  
   139  // formats callers: "<pkg/lib/cli/command/command.go:232>"
   140  func fmtCaller(e genericr.Entry) string {
   141  	return "<" + e.Caller.File + ":" + strconv.Itoa(e.Caller.Line) + "> "
   142  }
   143  
   144  // pretty controls the formatting of arbitrary log values into strings that
   145  // we can print. some types of values (eg maps and slices) are rendered as a
   146  // multiline string.
   147  //
   148  // when indenting various sections of multiline values, tabs are converted to
   149  // spaces for portability. '\t' character should only be used via a tabwriter
   150  // for aligning arbitrary key value pairs, because the tabwriter package
   151  // converts to spaces under the covers.
   152  //
   153  // TODO(aw185176): This is still very limited, it needs to handle more complex data types
   154  //
   155  //nolint:gocyclo // Breaking down this function would reduce clarity and increase complexity
   156  func (l *clog) pretty(v any) (ret string) {
   157  	// Recover from panics if we end up calling a faulty interface implementation
   158  	defer func() {
   159  		if r := recover(); r != nil {
   160  			ret = fmt.Sprintf("<panic: %s>", r)
   161  		}
   162  	}()
   163  
   164  	// Handle types that take full control of logging.
   165  	if m, ok := v.(logr.Marshaler); ok {
   166  		// Replace the value with what the type wants to get logged.
   167  		// That then gets handled below via reflection.
   168  		v = m.MarshalLog()
   169  	}
   170  
   171  	// Handle types that want to format themselves.
   172  	switch value := v.(type) {
   173  	case fmt.Stringer:
   174  		v = value.String()
   175  	case error:
   176  		v = value.Error()
   177  	}
   178  
   179  	// Try handling the most common types without reflect
   180  	switch v := v.(type) {
   181  	case bool:
   182  		return strconv.FormatBool(v)
   183  	case string:
   184  		return prettyString(v)
   185  	case int:
   186  		return strconv.FormatInt(int64(v), 10)
   187  	case int8:
   188  		return strconv.FormatInt(int64(v), 10)
   189  	case int16:
   190  		return strconv.FormatInt(int64(v), 10)
   191  	case int32:
   192  		return strconv.FormatInt(int64(v), 10)
   193  	case int64:
   194  		return strconv.FormatInt(v, 10)
   195  	case uint:
   196  		return strconv.FormatUint(uint64(v), 10)
   197  	case uint8:
   198  		return strconv.FormatUint(uint64(v), 10)
   199  	case uint16:
   200  		return strconv.FormatUint(uint64(v), 10)
   201  	case uint32:
   202  		return strconv.FormatUint(uint64(v), 10)
   203  	case uint64:
   204  		return strconv.FormatUint(v, 10)
   205  	case uintptr:
   206  		return strconv.FormatUint(uint64(v), 10)
   207  	case float32:
   208  		return strconv.FormatFloat(float64(v), 'f', -1, 32)
   209  	case float64:
   210  		return strconv.FormatFloat(v, 'f', -1, 64)
   211  	case complex64:
   212  		return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"`
   213  	case complex128:
   214  		return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"`
   215  	}
   216  
   217  	t := reflect.TypeOf(v)
   218  	if t == nil {
   219  		return "null"
   220  	}
   221  	val := reflect.ValueOf(v)
   222  
   223  	// See if we were able to find simple type via reflection
   224  	switch t.Kind() {
   225  	case reflect.Bool:
   226  		return strconv.FormatBool(val.Bool())
   227  	case reflect.String:
   228  		return prettyString(val.String())
   229  	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
   230  		return strconv.FormatInt(val.Int(), 10)
   231  	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
   232  		return strconv.FormatUint(val.Uint(), 10)
   233  	case reflect.Float32:
   234  		return strconv.FormatFloat(val.Float(), 'f', -1, 32)
   235  	case reflect.Float64:
   236  		return strconv.FormatFloat(val.Float(), 'f', -1, 64)
   237  	}
   238  
   239  	// By waiting until after we check simple types we can defer initializing
   240  	// buffer until we know we need it
   241  	buf := bytes.NewBuffer(make([]byte, 0, 256))
   242  	switch t.Kind() {
   243  	case reflect.Slice, reflect.Array:
   244  		if val.Len() == 0 {
   245  			return "[]"
   246  		}
   247  
   248  		buf.WriteByte('\n')
   249  		for i := 0; i < val.Len(); i++ {
   250  			buf.WriteString(indent(l.tabWidth, 2))
   251  			buf.WriteString(l.pretty(val.Index(i).Interface()))
   252  			// Don't add trailing newline
   253  			if i < val.Len()-1 {
   254  				buf.WriteByte('\n')
   255  			}
   256  		}
   257  		return buf.String()
   258  	case reflect.Map:
   259  		if val.Len() == 0 {
   260  			return "{}"
   261  		}
   262  
   263  		// Keys must be sorted to give reliable map order
   264  		keys := make([]string, 0, val.Len())
   265  		vals := make(map[string]string, val.Len())
   266  
   267  		buf.WriteByte('\n')
   268  		it := val.MapRange()
   269  		i := 0
   270  		for it.Next() {
   271  			valstr := l.pretty(it.Value().Interface())
   272  			// Don't log empty map entries
   273  			if valstr == "" {
   274  				i++
   275  				continue
   276  			}
   277  
   278  			keystr := ""
   279  			// If the key supports TextMarshaler, use it
   280  			if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok {
   281  				txt, err := m.MarshalText()
   282  				if err != nil {
   283  					keystr = fmt.Sprintf("<error-MarshalText: %s>", err.Error())
   284  				} else {
   285  					keystr = string(txt)
   286  				}
   287  				keystr = prettyString(keystr)
   288  			} else {
   289  				// Otherwise pretty() should do well enough
   290  				keystr = l.pretty(it.Key().Interface())
   291  			}
   292  			keys = append(keys, keystr)
   293  			vals[keystr] = valstr
   294  			i++
   295  		}
   296  
   297  		tw := tabwriter.NewWriter(buf, 2, l.tabWidth, 2, ' ', 0)
   298  		sort.Strings(keys)
   299  		for i, k := range keys {
   300  			fmt.Fprintf(tw, "%s%s\t%s", indent(l.tabWidth, 2), k, vals[k])
   301  			// Don't add trailing newline
   302  			if i < len(keys)-1 {
   303  				fmt.Fprint(tw, "\n")
   304  			}
   305  		}
   306  		tw.Flush()
   307  		return buf.String()
   308  	}
   309  
   310  	return fmt.Sprintf("%+v", v)
   311  }
   312  
   313  func prettyString(s string) string {
   314  	// Avoid escaping (which does allocations) if we can.
   315  	if needsEscape(s) {
   316  		return strconv.Quote(s)
   317  	}
   318  	return s
   319  }
   320  
   321  // needsEscape determines whether the input string needs to be escaped or not,
   322  // without doing any allocations.
   323  func needsEscape(s string) bool {
   324  	for _, r := range s {
   325  		if !strconv.IsPrint(r) || r == '\\' || r == '"' {
   326  			return true
   327  		}
   328  	}
   329  	return false
   330  }
   331  
   332  // indent produces a string of spaces representing t tab width * n tabs, because
   333  // `\t` can be interpreted differently on different machines.
   334  func indent(t int, n int) string {
   335  	str := ""
   336  	for i := 0; i < n; i++ {
   337  		for ii := 0; ii < t; ii++ {
   338  			str += " "
   339  		}
   340  	}
   341  	return str
   342  }
   343  

View as plain text