Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions log/example_test.go
Original file line number Diff line number Diff line change
@@ -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"}
}
199 changes: 199 additions & 0 deletions log/log.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be protected by a mutex, or some way to serialize the writes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. I left it out in this early API sketch. We could lock a mutex around the call to w.Write.

Another approach is to give options to the app code by providing a SyncHandler and an AsyncHandler. Both would wrap an arbitrary Handler. SyncHandler protects the wrapped handler via a Mutex. AsyncHandler serializes the pipeline via a (maybe buffered) channel and introduces a goroutine to run the downstream handlers asynchronously.

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 }
84 changes: 84 additions & 0 deletions log/log_test.go
Original file line number Diff line number Diff line change
@@ -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
}