/* Copyright 2019 The logr Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package glogr implements github.com/go-logr/logr.Logger in terms of // github.com/golang/glog. package glogr import ( "bytes" "fmt" "path/filepath" "reflect" "runtime" "strconv" "strings" "sync/atomic" "github.com/go-logr/logr" "github.com/golang/glog" ) // New returns a logr.Logger which is implemented by glog. func New() logr.Logger { return NewWithOptions(Options{}) } // NewWithOptions returns a logr.Logger which is implemented by glog. func NewWithOptions(opts Options) logr.Logger { if opts.Depth < 0 { opts.Depth = 0 } gl := &glogger{ prefix: "", values: nil, depth: opts.Depth, logCaller: opts.LogCaller, } return logr.New(gl) } // Options carries parameters which influence the way logs are generated. type Options struct { // Depth biases the assumed number of call frames to the "true" caller. // This is useful when the calling code calls a function which then calls // glogr (e.g. a logging shim to another API). Values less than zero will // be treated as zero. Depth int // LogCaller tells glogr to add a "caller" key to some or all log lines. // The glog implementation always logs this information in its per-line // header, whether this option is set or not. LogCaller MessageClass // TODO: add an option to log the date/time } // MessageClass indicates which category or categories of messages to consider. type MessageClass int const ( None MessageClass = iota All Info Error ) type glogger struct { prefix string values []interface{} depth int logCaller MessageClass } var _ logr.LogSink = &glogger{} var _ logr.CallDepthLogSink = &glogger{} // Magic string for intermediate frames that we should ignore. const autogeneratedFrameName = "" // Cached depth of this interface's log functions. var framesAtomic int32 // atomic // Discover how many frames we need to climb to find the caller. This approach // was suggested by Ian Lance Taylor of the Go team, so it *should* be safe // enough (famous last words). // // This assumes that all logging paths are the same depth from the caller, // which should be a reasonable assumption since they are part of the same // interface. func framesToCaller() int { // Figuring out the current depth is somewhat expensive. Saving the value // amortizes most of that runtime cost. if atomic.LoadInt32(&framesAtomic) != 0 { return int(framesAtomic) } // 1 is the immediate caller. 3 should be too many. for i := 1; i < 3; i++ { _, file, _, _ := runtime.Caller(i + 1) // +1 for this function's frame if file != autogeneratedFrameName { atomic.StoreInt32(&framesAtomic, int32(i)) return i } } return 1 // something went wrong, this is safe } func flatten(kvList ...interface{}) string { if len(kvList)%2 != 0 { kvList = append(kvList, "") } // Empirically bytes.Buffer is faster than strings.Builder for this. buf := bytes.NewBuffer(make([]byte, 0, 1024)) for i := 0; i < len(kvList); i += 2 { k, ok := kvList[i].(string) if !ok { k = fmt.Sprintf("", i/2) } v := kvList[i+1] if i > 0 { buf.WriteRune(' ') } buf.WriteRune('"') buf.WriteString(k) buf.WriteRune('"') buf.WriteRune('=') buf.WriteString(pretty(v)) } return buf.String() } func pretty(value interface{}) string { return prettyWithFlags(value, 0) } const ( flagRawString = 0x1 ) // TODO: This is not fast. Most of the overhead goes here. func prettyWithFlags(value interface{}, flags uint32) string { // Handling the most common types without reflect is a small perf win. switch v := value.(type) { case bool: return strconv.FormatBool(v) case string: if flags&flagRawString > 0 { return v } // This is empirically faster than strings.Builder. return `"` + 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(int64(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) } buf := bytes.NewBuffer(make([]byte, 0, 256)) t := reflect.TypeOf(value) if t == nil { return "null" } v := reflect.ValueOf(value) switch t.Kind() { case reflect.Bool: return strconv.FormatBool(v.Bool()) case reflect.String: if flags&flagRawString > 0 { return v.String() } // This is empirically faster than strings.Builder. return `"` + v.String() + `"` case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(int64(v.Int()), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(uint64(v.Uint()), 10) case reflect.Float32: return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32) case reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64) case reflect.Struct: buf.WriteRune('{') for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.PkgPath != "" { // reflect says this field is only defined for non-exported fields. continue } if i > 0 { buf.WriteRune(',') } buf.WriteRune('"') name := f.Name if tag, found := f.Tag.Lookup("json"); found { if comma := strings.Index(tag, ","); comma != -1 { name = tag[:comma] } else { name = tag } } buf.WriteString(name) buf.WriteRune('"') buf.WriteRune(':') buf.WriteString(pretty(v.Field(i).Interface())) } buf.WriteRune('}') return buf.String() case reflect.Slice, reflect.Array: buf.WriteRune('[') for i := 0; i < v.Len(); i++ { if i > 0 { buf.WriteRune(',') } e := v.Index(i) buf.WriteString(pretty(e.Interface())) } buf.WriteRune(']') return buf.String() case reflect.Map: buf.WriteRune('{') // This does not sort the map keys, for best perf. it := v.MapRange() i := 0 for it.Next() { if i > 0 { buf.WriteRune(',') } // JSON only does string keys. buf.WriteRune('"') buf.WriteString(prettyWithFlags(it.Key().Interface(), flagRawString)) buf.WriteRune('"') buf.WriteRune(':') buf.WriteString(pretty(it.Value().Interface())) i++ } buf.WriteRune('}') return buf.String() case reflect.Ptr, reflect.Interface: return pretty(v.Elem().Interface()) } return fmt.Sprintf(`""`, t.Kind().String()) } type callerID struct { File string `json:"file"` Line int `json:"line"` } func (l glogger) caller() callerID { // +1 for this frame. _, file, line, ok := runtime.Caller(framesToCaller() + l.depth + 1) if !ok { return callerID{"", 0} } return callerID{filepath.Base(file), line} } func (l *glogger) Init(info logr.RuntimeInfo) { l.depth += info.CallDepth } func (l glogger) Enabled(level int) bool { return bool(glog.V(glog.Level(level))) } func (l glogger) Info(level int, msg string, kvList ...interface{}) { args := make([]interface{}, 0, 64) // using a constant here impacts perf if l.logCaller == All || l.logCaller == Info { args = append(args, "caller", l.caller()) } args = append(args, "level", level, "msg", msg) args = append(args, l.values...) args = append(args, kvList...) argsStr := flatten(args...) glog.InfoDepth(framesToCaller()+l.depth, l.prefix, argsStr) } func (l glogger) Error(err error, msg string, kvList ...interface{}) { args := make([]interface{}, 0, 64) // using a constant here impacts perf if l.logCaller == All || l.logCaller == Error { args = append(args, "caller", l.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, kvList...) argsStr := flatten(args...) glog.ErrorDepth(framesToCaller()+l.depth, l.prefix, argsStr) } // WithName returns a new logr.Logger with the specified name appended. glogr // 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 *glogger) WithName(name string) logr.LogSink { l2 := &glogger{} *l2 = *l if len(l2.prefix) > 0 { l.prefix = l2.prefix + "/" } l2.prefix += name return l2 } func (l *glogger) WithValues(kvList ...interface{}) logr.LogSink { l2 := &glogger{} *l2 = *l // Three slice args forces a copy. n := len(l.values) l2.values = append(l2.values[:n:n], kvList...) return l2 } func (l *glogger) WithCallDepth(depth int) logr.LogSink { l2 := &glogger{} *l2 = *l l2.depth += depth return l2 }