import (
"bufio"
+ "bytes"
+ "encoding"
"encoding/json"
"fmt"
"log"
"os"
+ "reflect"
"runtime"
+ "sort"
"strconv"
"strings"
"sync"
+ "sync/atomic"
"time"
)
_levelToBracket = map[Level]string{
Debug: "[DEBUG]",
Trace: "[TRACE]",
- Info: "[INFO ]",
- Warn: "[WARN ]",
+ Info: "[INFO] ",
+ Warn: "[WARN] ",
Error: "[ERROR]",
}
)
level = DefaultLevel
}
- return &intLogger{
- m: new(sync.Mutex),
- json: opts.JSONFormat,
- caller: opts.IncludeLocation,
- name: opts.Name,
- w: bufio.NewWriter(output),
- level: level,
+ mtx := opts.Mutex
+ if mtx == nil {
+ mtx = new(sync.Mutex)
}
+
+ ret := &intLogger{
+ m: mtx,
+ json: opts.JSONFormat,
+ caller: opts.IncludeLocation,
+ name: opts.Name,
+ timeFormat: TimeFormat,
+ w: bufio.NewWriter(output),
+ level: new(int32),
+ }
+ if opts.TimeFormat != "" {
+ ret.timeFormat = opts.TimeFormat
+ }
+ atomic.StoreInt32(ret.level, int32(level))
+ return ret
}
// The internal logger implementation. Internal in that it is defined entirely
// by this package.
type intLogger struct {
- json bool
- caller bool
- name string
+ json bool
+ caller bool
+ name string
+ timeFormat string
// this is a pointer so that it's shared by any derived loggers, since
// those derived loggers share the bufio.Writer as well.
m *sync.Mutex
w *bufio.Writer
- level Level
+ level *int32
implied []interface{}
}
// Log a message and a set of key/value pairs if the given level is at
// or more severe that the threshold configured in the Logger.
func (z *intLogger) Log(level Level, msg string, args ...interface{}) {
- if level < z.level {
+ if level < Level(atomic.LoadInt32(z.level)) {
return
}
// Non-JSON logging format function
func (z *intLogger) log(t time.Time, level Level, msg string, args ...interface{}) {
- z.w.WriteString(t.Format(TimeFormat))
+ z.w.WriteString(t.Format(z.timeFormat))
z.w.WriteByte(' ')
s, ok := _levelToBracket[level]
if ok {
z.w.WriteString(s)
} else {
- z.w.WriteString("[UNKN ]")
+ z.w.WriteString("[?????]")
}
if z.caller {
FOR:
for i := 0; i < len(args); i = i + 2 {
- var val string
+ var (
+ val string
+ raw bool
+ )
switch st := args[i+1].(type) {
case string:
case CapturedStacktrace:
stacktrace = st
continue FOR
+ case Format:
+ val = fmt.Sprintf(st[0].(string), st[1:]...)
default:
- val = fmt.Sprintf("%v", st)
+ v := reflect.ValueOf(st)
+ if v.Kind() == reflect.Slice {
+ val = z.renderSlice(v)
+ raw = true
+ } else {
+ val = fmt.Sprintf("%v", st)
+ }
}
z.w.WriteByte(' ')
z.w.WriteString(args[i].(string))
z.w.WriteByte('=')
- if strings.ContainsAny(val, " \t\n\r") {
+ if !raw && strings.ContainsAny(val, " \t\n\r") {
z.w.WriteByte('"')
z.w.WriteString(val)
z.w.WriteByte('"')
}
}
+func (z *intLogger) renderSlice(v reflect.Value) string {
+ var buf bytes.Buffer
+
+ buf.WriteRune('[')
+
+ for i := 0; i < v.Len(); i++ {
+ if i > 0 {
+ buf.WriteString(", ")
+ }
+
+ sv := v.Index(i)
+
+ var val string
+
+ switch sv.Kind() {
+ case reflect.String:
+ val = sv.String()
+ case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+ val = strconv.FormatInt(sv.Int(), 10)
+ case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ val = strconv.FormatUint(sv.Uint(), 10)
+ default:
+ val = fmt.Sprintf("%v", sv.Interface())
+ }
+
+ if strings.ContainsAny(val, " \t\n\r") {
+ buf.WriteByte('"')
+ buf.WriteString(val)
+ buf.WriteByte('"')
+ } else {
+ buf.WriteString(val)
+ }
+ }
+
+ buf.WriteRune(']')
+
+ return buf.String()
+}
+
// JSON logging function
func (z *intLogger) logJson(t time.Time, level Level, msg string, args ...interface{}) {
vals := map[string]interface{}{
}
}
+ args = append(z.implied, args...)
+
if args != nil && len(args) > 0 {
if len(args)%2 != 0 {
cs, ok := args[len(args)-1].(CapturedStacktrace)
// without injecting into logs...
continue
}
- vals[args[i].(string)] = args[i+1]
+ val := args[i+1]
+ switch sv := val.(type) {
+ case error:
+ // Check if val is of type error. If error type doesn't
+ // implement json.Marshaler or encoding.TextMarshaler
+ // then set val to err.Error() so that it gets marshaled
+ switch sv.(type) {
+ case json.Marshaler, encoding.TextMarshaler:
+ default:
+ val = sv.Error()
+ }
+ case Format:
+ val = fmt.Sprintf(sv[0].(string), sv[1:]...)
+ }
+
+ vals[args[i].(string)] = val
}
}
// Indicate that the logger would emit TRACE level logs
func (z *intLogger) IsTrace() bool {
- return z.level == Trace
+ return Level(atomic.LoadInt32(z.level)) == Trace
}
// Indicate that the logger would emit DEBUG level logs
func (z *intLogger) IsDebug() bool {
- return z.level <= Debug
+ return Level(atomic.LoadInt32(z.level)) <= Debug
}
// Indicate that the logger would emit INFO level logs
func (z *intLogger) IsInfo() bool {
- return z.level <= Info
+ return Level(atomic.LoadInt32(z.level)) <= Info
}
// Indicate that the logger would emit WARN level logs
func (z *intLogger) IsWarn() bool {
- return z.level <= Warn
+ return Level(atomic.LoadInt32(z.level)) <= Warn
}
// Indicate that the logger would emit ERROR level logs
func (z *intLogger) IsError() bool {
- return z.level <= Error
+ return Level(atomic.LoadInt32(z.level)) <= Error
}
// Return a sub-Logger for which every emitted log message will contain
// the given key/value pairs. This is used to create a context specific
// Logger.
func (z *intLogger) With(args ...interface{}) Logger {
+ if len(args)%2 != 0 {
+ panic("With() call requires paired arguments")
+ }
+
var nz intLogger = *z
- nz.implied = append(nz.implied, args...)
+ result := make(map[string]interface{}, len(z.implied)+len(args))
+ keys := make([]string, 0, len(z.implied)+len(args))
+
+ // Read existing args, store map and key for consistent sorting
+ for i := 0; i < len(z.implied); i += 2 {
+ key := z.implied[i].(string)
+ keys = append(keys, key)
+ result[key] = z.implied[i+1]
+ }
+ // Read new args, store map and key for consistent sorting
+ for i := 0; i < len(args); i += 2 {
+ key := args[i].(string)
+ _, exists := result[key]
+ if !exists {
+ keys = append(keys, key)
+ }
+ result[key] = args[i+1]
+ }
+
+ // Sort keys to be consistent
+ sort.Strings(keys)
+
+ nz.implied = make([]interface{}, 0, len(z.implied)+len(args))
+ for _, k := range keys {
+ nz.implied = append(nz.implied, k)
+ nz.implied = append(nz.implied, result[k])
+ }
return &nz
}
if nz.name != "" {
nz.name = nz.name + "." + name
+ } else {
+ nz.name = name
}
return &nz
return &nz
}
+// Update the logging level on-the-fly. This will affect all subloggers as
+// well.
+func (z *intLogger) SetLevel(level Level) {
+ atomic.StoreInt32(z.level, int32(level))
+}
+
// Create a *log.Logger that will send it's data through this Logger. This
// allows packages that expect to be using the standard library log to actually
// use this logger.