diff --git a/log/example_test.go b/log/example_test.go new file mode 100644 index 000000000..e15b588f0 --- /dev/null +++ b/log/example_test.go @@ -0,0 +1,42 @@ +package log_test + +import ( + "os" + "time" + + "github.com/peterbourgon/gokit/log" +) + +func Example_log() { + h := log.Writer(os.Stdout, log.JsonEncoder()) + h = log.ReplaceKeys(h, log.TimeKey, "t", log.LvlKey, "lvl") + + // Use a static time so it will always match the output below + now := time.Date(2015, time.March, 7, 20, 12, 33, 0, time.UTC) + + // A real program would add timestamps in the handler chain + // h = log.AddTimestamp(h) + + l := log.New() + l.SetHandler(h) + + l.Log(log.Info, log.TimeKey, now, "msg", "Hello, world!") + // Output: + // {"lvl":"info","msg":"Hello, world!","t":"2015-03-07T20:12:33Z"} +} + +func Example_logContext() { + h := log.Writer(os.Stdout, log.JsonEncoder()) + h = log.ReplaceKeys(h, log.LvlKey, "lvl") + + // Create a logger with some default KVs + l := log.New("host", "hal9000") + l.SetHandler(h) + + // Create another new logger from the first, adding another KV. + l2 := l.New("url", "/index.html") + + l2.Log(log.Info, "msg", "Hello, world!") + // Output: + // {"host":"hal9000","lvl":"info","msg":"Hello, world!","url":"/index.html"} +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 000000000..92717fd3b --- /dev/null +++ b/log/log.go @@ -0,0 +1,199 @@ +package log + +import ( + "encoding/json" + "fmt" + "io" + "time" +) + +type Logger interface { + New(keyvals ...interface{}) Logger + SetHandler(h Handler) + Log(keyvals ...interface{}) +} + +type logger struct { + ctx []interface{} + h Handler +} + +func New(keyvals ...interface{}) Logger { + return &logger{ + ctx: expandKeyVals(keyvals), + } +} + +func (l *logger) New(keyvals ...interface{}) Logger { + var kv []interface{} + kv = append(kv, l.ctx...) + kv = append(kv, expandKeyVals(keyvals)...) + + return &logger{ + ctx: kv, + h: l.h, + } +} + +func (l *logger) SetHandler(h Handler) { + l.h = h +} + +func (l *logger) Log(keyvals ...interface{}) { + var kv []interface{} + kv = append(kv, l.ctx...) + // kv = append(keyvals, CalllerKey, stack.Caller(1)) + kv = append(kv, expandKeyVals(keyvals)...) + l.h.Handle(kv...) +} + +func expandKeyVals(keyvals []interface{}) []interface{} { + kvCount := len(keyvals) + for _, kv := range keyvals { + if _, ok := kv.(KeyVal); ok { + kvCount++ + } + } + if kvCount == len(keyvals) { + return keyvals + } + exp := make([]interface{}, 0, kvCount) + for _, kv := range keyvals { + switch kv := kv.(type) { + case KeyVal: + exp = append(exp, kv.Key()) + exp = append(exp, kv.Value()) + default: + exp = append(exp, kv) + } + } + return exp +} + +type KeyVal interface { + Key() interface{} + Value() interface{} +} + +type key int + +const ( + TimeKey key = iota + LvlKey + CalllerKey +) + +func Now() KeyVal { + return logtime(time.Now()) +} + +type logtime time.Time + +func (t logtime) Key() interface{} { return TimeKey } +func (t logtime) Value() interface{} { return time.Time(t) } + +type Lvl int + +const ( + Crit Lvl = iota + Error + Warn + Info + Debug +) + +func (l Lvl) Key() interface{} { return LvlKey } +func (l Lvl) Value() interface{} { return l } + +func (l Lvl) MarshalText() (text []byte, err error) { + switch l { + case Crit: + return []byte("crit"), nil + case Error: + return []byte("eror"), nil + case Warn: + return []byte("warn"), nil + case Info: + return []byte("info"), nil + case Debug: + return []byte("dbug"), nil + } + panic("unexpected level value") +} + +type Handler interface { + Handle(keyvals ...interface{}) error +} + +type HandlerFunc func(keyvals ...interface{}) error + +func (f HandlerFunc) Handle(keyvals ...interface{}) error { + return f(keyvals...) +} + +type Encoder interface { + Encode(keyvals ...interface{}) ([]byte, error) +} + +func AddTimestamp(h Handler) Handler { + return HandlerFunc(func(keyvals ...interface{}) error { + kvs := append(keyvals, nil, nil) + copy(kvs[2:], kvs[:len(kvs)-2]) + kvs[0] = TimeKey + kvs[1] = time.Now() + return h.Handle(kvs...) + }) +} + +func ReplaceKeys(h Handler, keypairs ...interface{}) Handler { + m := make(map[interface{}]interface{}, len(keypairs)/2) + for i := 0; i < len(keypairs); i += 2 { + m[keypairs[i]] = keypairs[i+1] + } + return HandlerFunc(func(keyvals ...interface{}) error { + for i := 0; i < len(keyvals); i += 2 { + if nk, ok := m[keyvals[i]]; ok { + keyvals[i] = nk + } + } + return h.Handle(keyvals...) + }) +} + +func Writer(w io.Writer, enc Encoder) Handler { + return HandlerFunc(func(keyvals ...interface{}) error { + b, err := enc.Encode(keyvals...) + if err != nil { + return err + } + _, err = w.Write(b) + return err + }) +} + +type EncoderFunc func(keyvals ...interface{}) ([]byte, error) + +func (f EncoderFunc) Encode(keyvals ...interface{}) ([]byte, error) { + return f(keyvals...) +} + +func JsonEncoder() Encoder { + return EncoderFunc(func(keyvals ...interface{}) ([]byte, error) { + m := make(map[string]interface{}, len(keyvals)/2) + for i := 0; i < len(keyvals); i += 2 { + m[fmt.Sprint(keyvals[i])] = keyvals[i+1] + } + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + return b, nil + }) +} + +// func Failover(alternates ...Handler) Handler { return nil } +// func LvlFilter(maxLvl Lvl, h Handler) Handler { return nil } +// func Multiple(hs ...Handler) Handler { return nil } + +// func Discard() Handler { return nil } +// func Encode(enc Encoder, w io.Writer) Handler { return nil } diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 000000000..6e32e94ff --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,84 @@ +package log_test + +import ( + "reflect" + "testing" + + "github.com/peterbourgon/gokit/log" +) + +func TestLogger(t *testing.T) { + want := [][]interface{}{ + {"msg", "Hello, world!"}, + } + + got := [][]interface{}{} + l := sliceLogger(&got) + + for _, d := range want { + l.Log(d...) + } + + for i := range want { + if g, w := got[i], want[i]; !reflect.DeepEqual(g, w) { + t.Errorf("\n got %v\nwant %v", g, w) + } + } +} + +func TestKeyValue(t *testing.T) { + now := log.Now() + + data := []struct { + in []interface{} + out []interface{} + }{ + { + in: []interface{}{log.Debug}, + out: []interface{}{log.LvlKey, log.Debug}, + }, + { + in: []interface{}{log.Info}, + out: []interface{}{log.LvlKey, log.Info}, + }, + { + in: []interface{}{log.Warn}, + out: []interface{}{log.LvlKey, log.Warn}, + }, + { + in: []interface{}{log.Error}, + out: []interface{}{log.LvlKey, log.Error}, + }, + { + in: []interface{}{log.Crit}, + out: []interface{}{log.LvlKey, log.Crit}, + }, + { + in: []interface{}{now}, + out: []interface{}{log.TimeKey, now.Value()}, + }, + } + + got := [][]interface{}{} + l := sliceLogger(&got) + + for _, d := range data { + l.Log(d.in...) + } + + for i := range data { + if g, w := got[i], data[i].out; !reflect.DeepEqual(g, w) { + t.Errorf("\n got %v\nwant %v", g, w) + } + } +} + +func sliceLogger(s *[][]interface{}) log.Logger { + l := log.New() + *s = [][]interface{}{} + l.SetHandler(log.HandlerFunc(func(keyvals ...interface{}) error { + *s = append(*s, keyvals) + return nil + })) + return l +}