...

Source file src/github.com/wojas/genericr/genericr.go

Documentation: github.com/wojas/genericr

     1  /*
     2  Copyright 2019 The logr Authors.
     3  Copyright 2020 The genericr Authors.
     4  
     5  Licensed under the Apache License, Version 2.0 (the "License");
     6  you may not use this file except in compliance with the License.
     7  You may obtain a copy of the License at
     8  
     9      http://www.apache.org/licenses/LICENSE-2.0
    10  
    11  Unless required by applicable law or agreed to in writing, software
    12  distributed under the License is distributed on an "AS IS" BASIS,
    13  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  See the License for the specific language governing permissions and
    15  limitations under the License.
    16  */
    17  
    18  // Package genericr implements github.com/go-logr/logr.LogSink in a generic way
    19  // that allows easy implementation of other logging backends.
    20  package genericr
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"path/filepath"
    27  	"regexp"
    28  	"runtime"
    29  	"sort"
    30  	"strconv"
    31  
    32  	"github.com/go-logr/logr"
    33  )
    34  
    35  // Entry is a log entry that your adapter will receive for actual logging
    36  type Entry struct {
    37  	Level     int           // level at which this was logged
    38  	Name      string        // name parts joined with '.'
    39  	NameParts []string      // individual name segments
    40  	Message   string        // message as send to log call
    41  	Error     error         // error if .Error() was called
    42  	Fields    []interface{} // alternating key-value pairs
    43  
    44  	// Caller information
    45  	Caller      runtime.Frame // only available after .WithCaller(true)
    46  	CallerDepth int           // caller depth from callback
    47  }
    48  
    49  // String converts the entry to a string.
    50  // The output format is subject to change! Implement your own conversion if
    51  // you need to parse these logs later!
    52  // TODO: Neater way to log values with newlines?
    53  func (e Entry) String() string {
    54  	buf := bytes.NewBuffer(make([]byte, 0, 160))
    55  	buf.WriteByte('[')
    56  	buf.WriteString(strconv.Itoa(e.Level))
    57  	buf.WriteByte(']')
    58  	buf.WriteByte(' ')
    59  	if e.Caller.File != "" || e.Caller.Line != 0 {
    60  		buf.WriteString(e.CallerShort())
    61  		buf.WriteByte(' ')
    62  	}
    63  	buf.WriteString(e.Name)
    64  	buf.WriteByte(' ')
    65  	buf.WriteString(pretty(e.Message))
    66  	if e.Error != nil {
    67  		buf.WriteString(" error=")
    68  		buf.WriteString(pretty(e.Error.Error()))
    69  	}
    70  	if len(e.Fields) > 0 {
    71  		buf.WriteByte(' ')
    72  		buf.WriteString(flatten(e.Fields...))
    73  	}
    74  	return buf.String()
    75  }
    76  
    77  // FieldsMap converts the fields to a map.
    78  // This map is also compatible with logrus.Fields.
    79  func (e Entry) FieldsMap() map[string]interface{} {
    80  	return fieldsMap(e.Fields)
    81  }
    82  
    83  // CallerShort returns a short caller location string ("somefile.go:123")
    84  func (e Entry) CallerShort() string {
    85  	if e.Caller.File == "" && e.Caller.Line == 0 {
    86  		return ""
    87  	}
    88  	_, fname := filepath.Split(e.Caller.File)
    89  	return fmt.Sprintf("%s:%d", fname, e.Caller.Line)
    90  }
    91  
    92  // LogFunc is your custom log backend
    93  type LogFunc func(e Entry)
    94  
    95  // New returns a logr.LogSink which is implemented by your custom LogFunc.
    96  func New(f LogFunc) LogSink {
    97  	log := LogSink{
    98  		f:         f,
    99  		verbosity: 1000,
   100  	}
   101  	return log
   102  }
   103  
   104  // LogSink is a generic logger that implements the logr.LogSink interface and
   105  // calls a function of type LogFunc for every log message received.
   106  type LogSink struct {
   107  	f         LogFunc
   108  	level     int           // current verbosity level
   109  	verbosity int           // max verbosity level that we log
   110  	nameParts []string      // list of names
   111  	name      string        // nameParts joined by '.' for performance
   112  	values    []interface{} // key-value pairs
   113  	caller    bool          // try to retrieve the caller from the stack
   114  	depth     int           // call stack depth offset to figure out caller info
   115  }
   116  
   117  // WithVerbosity returns a new instance with given max verbosity level.
   118  // This is not part of the logr interface, so you can only use this on the root object.
   119  func (l LogSink) WithVerbosity(level int) LogSink {
   120  	l.verbosity = level
   121  	return l
   122  }
   123  
   124  // WithCaller enables or disables caller lookup for Entry.Caller.
   125  // It is disabled by default.
   126  // Local benchmarks show close to 1µs and 2 allocs extra overhead from enabling this,
   127  // without actually using this extra information.
   128  // This is not part of the logr interface, so you can only use this on the root object.
   129  func (l LogSink) WithCaller(enabled bool) LogSink {
   130  	l.caller = enabled
   131  	return l
   132  }
   133  
   134  // WithCallDepth implements logr.CallDepthLogSink.
   135  func (l LogSink) WithCallDepth(depth int) logr.LogSink {
   136  	l.depth += depth
   137  	return l
   138  }
   139  
   140  func (l LogSink) Init(info logr.RuntimeInfo) {
   141  }
   142  
   143  func (l LogSink) Info(level int, msg string, kvList ...interface{}) {
   144  	l.logMessage(level, nil, msg, kvList)
   145  }
   146  
   147  func (l LogSink) Enabled(level int) bool {
   148  	return l.verbosity >= level
   149  }
   150  
   151  func (l LogSink) Error(err error, msg string, kvList ...interface{}) {
   152  	l.logMessage(0, err, msg, kvList)
   153  }
   154  
   155  func (l LogSink) WithName(name string) logr.LogSink {
   156  	// We keep both a list of parts for full flexibility, and a pre-joined string
   157  	// for performance. We assume that this method is called far less often
   158  	// than that actual logging is done.
   159  	if len(l.nameParts) == 0 {
   160  		l.nameParts = []string{name}
   161  		l.name = name
   162  	} else {
   163  		n := len(l.nameParts)
   164  		l.nameParts = append(l.nameParts[:n:n], name) // triple-slice to force copy
   165  		l.name += "." + name
   166  	}
   167  	return l
   168  }
   169  
   170  func (l LogSink) WithValues(kvList ...interface{}) logr.LogSink {
   171  	if len(kvList) == 0 {
   172  		return l
   173  	}
   174  	if len(kvList)%2 == 1 {
   175  		// Ensure an odd number of items here does not corrupt the list
   176  		kvList = append(kvList, nil)
   177  	}
   178  	if len(l.values) == 0 {
   179  		l.values = kvList
   180  	} else {
   181  		n := len(l.values)
   182  		l.values = append(l.values[:n:n], kvList...) // triple-slice to force copy
   183  	}
   184  	return l
   185  }
   186  
   187  // logMessage implements the actual logging for .Info() and .Error()
   188  func (l LogSink) logMessage(level int, err error, msg string, kvList []interface{}) {
   189  	var out []interface{}
   190  	if len(l.values) == 0 && len(kvList) > 0 {
   191  		out = kvList
   192  	} else if len(l.values) > 0 && len(kvList) == 0 {
   193  		out = l.values
   194  	} else {
   195  		out = make([]interface{}, len(l.values)+len(kvList))
   196  		copy(out, l.values)
   197  		copy(out[len(l.values):], kvList)
   198  	}
   199  
   200  	calldepth := 3 + l.depth
   201  	var caller runtime.Frame
   202  	if l.caller {
   203  		pc := make([]uintptr, 1)
   204  		if n := runtime.Callers(calldepth+1, pc[:]); n >= 1 {
   205  			caller, _ = runtime.CallersFrames(pc).Next()
   206  		}
   207  	}
   208  
   209  	l.f(Entry{
   210  		Level:       level,
   211  		Name:        l.name,
   212  		NameParts:   l.nameParts,
   213  		Message:     msg,
   214  		Error:       err,
   215  		Fields:      out,
   216  		Caller:      caller,
   217  		CallerDepth: calldepth + 1, // +1 for callback
   218  	})
   219  }
   220  
   221  // Check that we indeed implement the logr.LogSink and logr.CallDepthLogSink interfaces.
   222  var _ logr.LogSink = LogSink{}
   223  var _ logr.CallDepthLogSink = LogSink{}
   224  
   225  // Helper functions below
   226  
   227  var safeString = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
   228  
   229  func prettyKey(v string) string {
   230  	if safeString.MatchString(v) {
   231  		return v
   232  	} else {
   233  		return pretty(v)
   234  	}
   235  }
   236  
   237  func pretty(value interface{}) string {
   238  	switch v := value.(type) {
   239  	case error:
   240  		value = v.Error()
   241  	case []byte:
   242  		return fmt.Sprintf(`"% x"`, v)
   243  	}
   244  	jb, err := json.Marshal(value)
   245  	if err != nil {
   246  		jb, _ = json.Marshal(fmt.Sprintf("%q", value))
   247  	}
   248  	return string(jb)
   249  }
   250  
   251  // flatten converts a key-value list to a friendly string
   252  func flatten(kvList ...interface{}) string {
   253  	vals := fieldsMap(kvList)
   254  	keys := make([]string, 0, len(vals))
   255  	for k := range vals {
   256  		keys = append(keys, k)
   257  	}
   258  	sort.Strings(keys)
   259  	buf := bytes.Buffer{}
   260  	for i, k := range keys {
   261  		v := vals[k]
   262  		if i > 0 {
   263  			buf.WriteRune(' ')
   264  		}
   265  		buf.WriteString(prettyKey(k))
   266  		buf.WriteString("=")
   267  		buf.WriteString(pretty(v))
   268  	}
   269  	return buf.String()
   270  }
   271  
   272  // fieldsMap converts the fields to a map.
   273  func fieldsMap(fields []interface{}) map[string]interface{} {
   274  	m := make(map[string]interface{}, len(fields))
   275  	for i := 0; i < len(fields); i += 2 {
   276  		k, ok := fields[i].(string)
   277  		if !ok {
   278  			k = fmt.Sprintf("!(%#v)", fields[i])
   279  		}
   280  		var v interface{}
   281  		if i+1 < len(fields) {
   282  			v = fields[i+1]
   283  		}
   284  		m[k] = v
   285  	}
   286  	return m
   287  }
   288  

View as plain text