github.com/aidoskuneen/adk-node@v0.0.0-20220315131952-2e32567cb7f4/log/format.go (about)

     1  package log
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/big"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  	"unicode/utf8"
    15  )
    16  
    17  const (
    18  	timeFormat        = "2006-01-02T15:04:05-0700"
    19  	termTimeFormat    = "01-02|15:04:05.000"
    20  	floatFormat       = 'f'
    21  	termMsgJust       = 40
    22  	termCtxMaxPadding = 40
    23  )
    24  
    25  // locationTrims are trimmed for display to avoid unwieldy log lines.
    26  var locationTrims = []string{
    27  	"github.com/aidoskuneen/adk-node/",
    28  }
    29  
    30  // PrintOrigins sets or unsets log location (file:line) printing for terminal
    31  // format output.
    32  func PrintOrigins(print bool) {
    33  	if print {
    34  		atomic.StoreUint32(&locationEnabled, 1)
    35  	} else {
    36  		atomic.StoreUint32(&locationEnabled, 0)
    37  	}
    38  }
    39  
    40  // locationEnabled is an atomic flag controlling whether the terminal formatter
    41  // should append the log locations too when printing entries.
    42  var locationEnabled uint32
    43  
    44  // locationLength is the maxmimum path length encountered, which all logs are
    45  // padded to to aid in alignment.
    46  var locationLength uint32
    47  
    48  // fieldPadding is a global map with maximum field value lengths seen until now
    49  // to allow padding log contexts in a bit smarter way.
    50  var fieldPadding = make(map[string]int)
    51  
    52  // fieldPaddingLock is a global mutex protecting the field padding map.
    53  var fieldPaddingLock sync.RWMutex
    54  
    55  type Format interface {
    56  	Format(r *Record) []byte
    57  }
    58  
    59  // FormatFunc returns a new Format object which uses
    60  // the given function to perform record formatting.
    61  func FormatFunc(f func(*Record) []byte) Format {
    62  	return formatFunc(f)
    63  }
    64  
    65  type formatFunc func(*Record) []byte
    66  
    67  func (f formatFunc) Format(r *Record) []byte {
    68  	return f(r)
    69  }
    70  
    71  // TerminalStringer is an analogous interface to the stdlib stringer, allowing
    72  // own types to have custom shortened serialization formats when printed to the
    73  // screen.
    74  type TerminalStringer interface {
    75  	TerminalString() string
    76  }
    77  
    78  // TerminalFormat formats log records optimized for human readability on
    79  // a terminal with color-coded level output and terser human friendly timestamp.
    80  // This format should only be used for interactive programs or while developing.
    81  //
    82  //     [LEVEL] [TIME] MESSAGE key=value key=value ...
    83  //
    84  // Example:
    85  //
    86  //     [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002
    87  //
    88  func TerminalFormat(usecolor bool) Format {
    89  	return FormatFunc(func(r *Record) []byte {
    90  		var color = 0
    91  		if usecolor {
    92  			switch r.Lvl {
    93  			case LvlCrit:
    94  				color = 35
    95  			case LvlError:
    96  				color = 31
    97  			case LvlWarn:
    98  				color = 33
    99  			case LvlInfo:
   100  				color = 32
   101  			case LvlDebug:
   102  				color = 36
   103  			case LvlTrace:
   104  				color = 34
   105  			}
   106  		}
   107  
   108  		b := &bytes.Buffer{}
   109  		lvl := r.Lvl.AlignedString()
   110  		if atomic.LoadUint32(&locationEnabled) != 0 {
   111  			// Log origin printing was requested, format the location path and line number
   112  			location := fmt.Sprintf("%+v", r.Call)
   113  			for _, prefix := range locationTrims {
   114  				location = strings.TrimPrefix(location, prefix)
   115  			}
   116  			// Maintain the maximum location length for fancyer alignment
   117  			align := int(atomic.LoadUint32(&locationLength))
   118  			if align < len(location) {
   119  				align = len(location)
   120  				atomic.StoreUint32(&locationLength, uint32(align))
   121  			}
   122  			padding := strings.Repeat(" ", align-len(location))
   123  
   124  			// Assemble and print the log heading
   125  			if color > 0 {
   126  				fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg)
   127  			} else {
   128  				fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg)
   129  			}
   130  		} else {
   131  			if color > 0 {
   132  				fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg)
   133  			} else {
   134  				fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg)
   135  			}
   136  		}
   137  		// try to justify the log output for short messages
   138  		length := utf8.RuneCountInString(r.Msg)
   139  		if len(r.Ctx) > 0 && length < termMsgJust {
   140  			b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length))
   141  		}
   142  		// print the keys logfmt style
   143  		logfmt(b, r.Ctx, color, true)
   144  		return b.Bytes()
   145  	})
   146  }
   147  
   148  // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable
   149  // format for key/value pairs.
   150  //
   151  // For more details see: http://godoc.org/github.com/kr/logfmt
   152  //
   153  func LogfmtFormat() Format {
   154  	return FormatFunc(func(r *Record) []byte {
   155  		common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg}
   156  		buf := &bytes.Buffer{}
   157  		logfmt(buf, append(common, r.Ctx...), 0, false)
   158  		return buf.Bytes()
   159  	})
   160  }
   161  
   162  func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) {
   163  	for i := 0; i < len(ctx); i += 2 {
   164  		if i != 0 {
   165  			buf.WriteByte(' ')
   166  		}
   167  
   168  		k, ok := ctx[i].(string)
   169  		v := formatLogfmtValue(ctx[i+1], term)
   170  		if !ok {
   171  			k, v = errorKey, formatLogfmtValue(k, term)
   172  		}
   173  
   174  		// XXX: we should probably check that all of your key bytes aren't invalid
   175  		fieldPaddingLock.RLock()
   176  		padding := fieldPadding[k]
   177  		fieldPaddingLock.RUnlock()
   178  
   179  		length := utf8.RuneCountInString(v)
   180  		if padding < length && length <= termCtxMaxPadding {
   181  			padding = length
   182  
   183  			fieldPaddingLock.Lock()
   184  			fieldPadding[k] = padding
   185  			fieldPaddingLock.Unlock()
   186  		}
   187  		if color > 0 {
   188  			fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k)
   189  		} else {
   190  			buf.WriteString(k)
   191  			buf.WriteByte('=')
   192  		}
   193  		buf.WriteString(v)
   194  		if i < len(ctx)-2 && padding > length {
   195  			buf.Write(bytes.Repeat([]byte{' '}, padding-length))
   196  		}
   197  	}
   198  	buf.WriteByte('\n')
   199  }
   200  
   201  // JSONFormat formats log records as JSON objects separated by newlines.
   202  // It is the equivalent of JSONFormatEx(false, true).
   203  func JSONFormat() Format {
   204  	return JSONFormatEx(false, true)
   205  }
   206  
   207  // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true,
   208  // records will be pretty-printed. If lineSeparated is true, records
   209  // will be logged with a new line between each record.
   210  func JSONFormatOrderedEx(pretty, lineSeparated bool) Format {
   211  	jsonMarshal := json.Marshal
   212  	if pretty {
   213  		jsonMarshal = func(v interface{}) ([]byte, error) {
   214  			return json.MarshalIndent(v, "", "    ")
   215  		}
   216  	}
   217  	return FormatFunc(func(r *Record) []byte {
   218  		props := make(map[string]interface{})
   219  
   220  		props[r.KeyNames.Time] = r.Time
   221  		props[r.KeyNames.Lvl] = r.Lvl.String()
   222  		props[r.KeyNames.Msg] = r.Msg
   223  
   224  		ctx := make([]string, len(r.Ctx))
   225  		for i := 0; i < len(r.Ctx); i += 2 {
   226  			k, ok := r.Ctx[i].(string)
   227  			if !ok {
   228  				props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i])
   229  			}
   230  			ctx[i] = k
   231  			ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true)
   232  		}
   233  		props[r.KeyNames.Ctx] = ctx
   234  
   235  		b, err := jsonMarshal(props)
   236  		if err != nil {
   237  			b, _ = jsonMarshal(map[string]string{
   238  				errorKey: err.Error(),
   239  			})
   240  			return b
   241  		}
   242  		if lineSeparated {
   243  			b = append(b, '\n')
   244  		}
   245  		return b
   246  	})
   247  }
   248  
   249  // JSONFormatEx formats log records as JSON objects. If pretty is true,
   250  // records will be pretty-printed. If lineSeparated is true, records
   251  // will be logged with a new line between each record.
   252  func JSONFormatEx(pretty, lineSeparated bool) Format {
   253  	jsonMarshal := json.Marshal
   254  	if pretty {
   255  		jsonMarshal = func(v interface{}) ([]byte, error) {
   256  			return json.MarshalIndent(v, "", "    ")
   257  		}
   258  	}
   259  
   260  	return FormatFunc(func(r *Record) []byte {
   261  		props := make(map[string]interface{})
   262  
   263  		props[r.KeyNames.Time] = r.Time
   264  		props[r.KeyNames.Lvl] = r.Lvl.String()
   265  		props[r.KeyNames.Msg] = r.Msg
   266  
   267  		for i := 0; i < len(r.Ctx); i += 2 {
   268  			k, ok := r.Ctx[i].(string)
   269  			if !ok {
   270  				props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i])
   271  			}
   272  			props[k] = formatJSONValue(r.Ctx[i+1])
   273  		}
   274  
   275  		b, err := jsonMarshal(props)
   276  		if err != nil {
   277  			b, _ = jsonMarshal(map[string]string{
   278  				errorKey: err.Error(),
   279  			})
   280  			return b
   281  		}
   282  
   283  		if lineSeparated {
   284  			b = append(b, '\n')
   285  		}
   286  
   287  		return b
   288  	})
   289  }
   290  
   291  func formatShared(value interface{}) (result interface{}) {
   292  	defer func() {
   293  		if err := recover(); err != nil {
   294  			if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
   295  				result = "nil"
   296  			} else {
   297  				panic(err)
   298  			}
   299  		}
   300  	}()
   301  
   302  	switch v := value.(type) {
   303  	case time.Time:
   304  		return v.Format(timeFormat)
   305  
   306  	case error:
   307  		return v.Error()
   308  
   309  	case fmt.Stringer:
   310  		return v.String()
   311  
   312  	default:
   313  		return v
   314  	}
   315  }
   316  
   317  func formatJSONValue(value interface{}) interface{} {
   318  	value = formatShared(value)
   319  	switch value.(type) {
   320  	case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
   321  		return value
   322  	default:
   323  		return fmt.Sprintf("%+v", value)
   324  	}
   325  }
   326  
   327  // formatValue formats a value for serialization
   328  func formatLogfmtValue(value interface{}, term bool) string {
   329  	if value == nil {
   330  		return "nil"
   331  	}
   332  
   333  	switch v := value.(type) {
   334  	case time.Time:
   335  		// Performance optimization: No need for escaping since the provided
   336  		// timeFormat doesn't have any escape characters, and escaping is
   337  		// expensive.
   338  		return v.Format(timeFormat)
   339  
   340  	case *big.Int:
   341  		// Big ints get consumed by the Stringer clause so we need to handle
   342  		// them earlier on.
   343  		if v == nil {
   344  			return "<nil>"
   345  		}
   346  		return formatLogfmtBigInt(v)
   347  	}
   348  	if term {
   349  		if s, ok := value.(TerminalStringer); ok {
   350  			// Custom terminal stringer provided, use that
   351  			return escapeString(s.TerminalString())
   352  		}
   353  	}
   354  	value = formatShared(value)
   355  	switch v := value.(type) {
   356  	case bool:
   357  		return strconv.FormatBool(v)
   358  	case float32:
   359  		return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
   360  	case float64:
   361  		return strconv.FormatFloat(v, floatFormat, 3, 64)
   362  	case int8:
   363  		return strconv.FormatInt(int64(v), 10)
   364  	case uint8:
   365  		return strconv.FormatInt(int64(v), 10)
   366  	case int16:
   367  		return strconv.FormatInt(int64(v), 10)
   368  	case uint16:
   369  		return strconv.FormatInt(int64(v), 10)
   370  	// Larger integers get thousands separators.
   371  	case int:
   372  		return FormatLogfmtInt64(int64(v))
   373  	case int32:
   374  		return FormatLogfmtInt64(int64(v))
   375  	case int64:
   376  		return FormatLogfmtInt64(v)
   377  	case uint:
   378  		return FormatLogfmtUint64(uint64(v))
   379  	case uint32:
   380  		return FormatLogfmtUint64(uint64(v))
   381  	case uint64:
   382  		return FormatLogfmtUint64(v)
   383  	case string:
   384  		return escapeString(v)
   385  	default:
   386  		return escapeString(fmt.Sprintf("%+v", value))
   387  	}
   388  }
   389  
   390  // FormatLogfmtInt64 formats n with thousand separators.
   391  func FormatLogfmtInt64(n int64) string {
   392  	if n < 0 {
   393  		return formatLogfmtUint64(uint64(-n), true)
   394  	}
   395  	return formatLogfmtUint64(uint64(n), false)
   396  }
   397  
   398  // FormatLogfmtUint64 formats n with thousand separators.
   399  func FormatLogfmtUint64(n uint64) string {
   400  	return formatLogfmtUint64(n, false)
   401  }
   402  
   403  func formatLogfmtUint64(n uint64, neg bool) string {
   404  	// Small numbers are fine as is
   405  	if n < 100000 {
   406  		if neg {
   407  			return strconv.Itoa(-int(n))
   408  		} else {
   409  			return strconv.Itoa(int(n))
   410  		}
   411  	}
   412  	// Large numbers should be split
   413  	const maxLength = 26
   414  
   415  	var (
   416  		out   = make([]byte, maxLength)
   417  		i     = maxLength - 1
   418  		comma = 0
   419  	)
   420  	for ; n > 0; i-- {
   421  		if comma == 3 {
   422  			comma = 0
   423  			out[i] = ','
   424  		} else {
   425  			comma++
   426  			out[i] = '0' + byte(n%10)
   427  			n /= 10
   428  		}
   429  	}
   430  	if neg {
   431  		out[i] = '-'
   432  		i--
   433  	}
   434  	return string(out[i+1:])
   435  }
   436  
   437  // formatLogfmtBigInt formats n with thousand separators.
   438  func formatLogfmtBigInt(n *big.Int) string {
   439  	if n.IsUint64() {
   440  		return FormatLogfmtUint64(n.Uint64())
   441  	}
   442  	if n.IsInt64() {
   443  		return FormatLogfmtInt64(n.Int64())
   444  	}
   445  
   446  	var (
   447  		text  = n.String()
   448  		buf   = make([]byte, len(text)+len(text)/3)
   449  		comma = 0
   450  		i     = len(buf) - 1
   451  	)
   452  	for j := len(text) - 1; j >= 0; j, i = j-1, i-1 {
   453  		c := text[j]
   454  
   455  		switch {
   456  		case c == '-':
   457  			buf[i] = c
   458  		case comma == 3:
   459  			buf[i] = ','
   460  			i--
   461  			comma = 0
   462  			fallthrough
   463  		default:
   464  			buf[i] = c
   465  			comma++
   466  		}
   467  	}
   468  	return string(buf[i+1:])
   469  }
   470  
   471  // escapeString checks if the provided string needs escaping/quoting, and
   472  // calls strconv.Quote if needed
   473  func escapeString(s string) string {
   474  	needsQuoting := false
   475  	for _, r := range s {
   476  		// We quote everything below " (0x34) and above~ (0x7E), plus equal-sign
   477  		if r <= '"' || r > '~' || r == '=' {
   478  			needsQuoting = true
   479  			break
   480  		}
   481  	}
   482  	if !needsQuoting {
   483  		return s
   484  	}
   485  	return strconv.Quote(s)
   486  }