-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
package log, alternate take #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8650f26
log: first draft of alternate implementation
peterbourgon 2a88002
log: some simplifications
peterbourgon 1b9a634
Prefer fmt.Fprint where possible
peterbourgon 9fae5e6
Prefer fmt.Fprintln where possible
peterbourgon 0c141e5
log: add Levels object
peterbourgon de65931
log: changes from feedback
peterbourgon 580420a
Resolve JSON logger error vals
peterbourgon f307245
Logger becomes one-method interface
peterbourgon dd0f49f
Add StdlibWriter adapter
peterbourgon 949eb09
log: StdlibWriter usage test
peterbourgon 5e3864f
Touch for CI rebuilds
peterbourgon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package log_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/peterbourgon/gokit/log" | ||
| ) | ||
|
|
||
| func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) { | ||
| logger = log.With(logger, "common_key", "common_value") | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| f(logger) | ||
| } | ||
| } | ||
|
|
||
| var ( | ||
| baseMessage = func(logger log.Logger) { logger.Log("foo_key", "foo_value") } | ||
| withMessage = func(logger log.Logger) { log.With(logger, "a", "b").Log("c", "d") } | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package log | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| ) | ||
|
|
||
| type jsonLogger struct { | ||
| io.Writer | ||
| } | ||
|
|
||
| // NewJSONLogger returns a Logger that encodes keyvals to the Writer as a | ||
| // single JSON object. | ||
| func NewJSONLogger(w io.Writer) Logger { | ||
| return &jsonLogger{w} | ||
| } | ||
|
|
||
| func (l *jsonLogger) Log(keyvals ...interface{}) error { | ||
| if len(keyvals)%2 == 1 { | ||
| panic("odd number of keyvals") | ||
| } | ||
| m := make(map[string]interface{}, len(keyvals)/2) | ||
| for i := 0; i < len(keyvals); i += 2 { | ||
| merge(m, keyvals[i], keyvals[i+1]) | ||
| } | ||
| return json.NewEncoder(l.Writer).Encode(m) | ||
| } | ||
|
|
||
| func merge(dst map[string]interface{}, k, v interface{}) map[string]interface{} { | ||
| var key string | ||
| switch x := k.(type) { | ||
| case string: | ||
| key = x | ||
| case fmt.Stringer: | ||
| key = x.String() | ||
| default: | ||
| key = fmt.Sprintf("%v", x) | ||
| } | ||
| if x, ok := v.(error); ok { | ||
| v = x.Error() | ||
| } | ||
| dst[key] = v | ||
| return dst | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package log_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "errors" | ||
| "io/ioutil" | ||
| "testing" | ||
|
|
||
| "github.com/peterbourgon/gokit/log" | ||
| ) | ||
|
|
||
| func TestJSONLogger(t *testing.T) { | ||
| buf := &bytes.Buffer{} | ||
| logger := log.NewJSONLogger(buf) | ||
| if err := logger.Log("err", errors.New("err"), "m", map[string]int{"0": 0}, "a", []int{1, 2, 3}); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if want, have := `{"a":[1,2,3],"err":"err","m":{"0":0}}`+"\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkJSONLoggerSimple(b *testing.B) { | ||
| benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard), baseMessage) | ||
| } | ||
|
|
||
| func BenchmarkJSONLoggerContextual(b *testing.B) { | ||
| benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard), withMessage) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package log | ||
|
|
||
| // Levels provides a default set of leveled loggers. | ||
| type Levels struct { | ||
| Debug Logger | ||
| Info Logger | ||
| Error Logger | ||
| } | ||
|
|
||
| type levelOptions struct { | ||
| levelKey string | ||
| debugValue string | ||
| infoValue string | ||
| errorValue string | ||
| } | ||
|
|
||
| // LevelOption sets a parameter for leveled loggers. | ||
| type LevelOption func(*levelOptions) | ||
|
|
||
| // LevelKey sets the key for the field used to indicate log level. By default, | ||
| // the key is "level". | ||
| func LevelKey(key string) LevelOption { | ||
| return func(o *levelOptions) { o.levelKey = key } | ||
| } | ||
|
|
||
| // DebugLevelValue sets the value for the field used to indicate the debug log | ||
| // level. By default, the value is "DEBUG". | ||
| func DebugLevelValue(value string) LevelOption { | ||
| return func(o *levelOptions) { o.debugValue = value } | ||
| } | ||
|
|
||
| // InfoLevelValue sets the value for the field used to indicate the debug log | ||
| // level. By default, the value is "INFO". | ||
| func InfoLevelValue(value string) LevelOption { | ||
| return func(o *levelOptions) { o.infoValue = value } | ||
| } | ||
|
|
||
| // ErrorLevelValue sets the value for the field used to indicate the debug log | ||
| // level. By default, the value is "ERROR". | ||
| func ErrorLevelValue(value string) LevelOption { | ||
| return func(o *levelOptions) { o.errorValue = value } | ||
| } | ||
|
|
||
| // NewLevels returns a new set of leveled loggers based on the base logger. | ||
| func NewLevels(base Logger, options ...LevelOption) Levels { | ||
| opts := &levelOptions{ | ||
| levelKey: "level", | ||
| debugValue: "DEBUG", | ||
| infoValue: "INFO", | ||
| errorValue: "ERROR", | ||
| } | ||
| for _, option := range options { | ||
| option(opts) | ||
| } | ||
| return Levels{ | ||
| Debug: With(base, opts.levelKey, opts.debugValue), | ||
| Info: With(base, opts.levelKey, opts.infoValue), | ||
| Error: With(base, opts.levelKey, opts.errorValue), | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package log_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/peterbourgon/gokit/log" | ||
| ) | ||
|
|
||
| func TestDefaultLevels(t *testing.T) { | ||
| buf := bytes.Buffer{} | ||
| levels := log.NewLevels(log.NewPrefixLogger(&buf)) | ||
|
|
||
| levels.Debug.Log("msg", "👨") // of course you'd want to do this | ||
| if want, have := "level=DEBUG msg=👨\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
|
|
||
| buf.Reset() | ||
| levels.Info.Log("msg", "🚀") | ||
| if want, have := "level=INFO msg=🚀\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
|
|
||
| buf.Reset() | ||
| levels.Error.Log("msg", "🍵") | ||
| if want, have := "level=ERROR msg=🍵\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
| } | ||
|
|
||
| func TestModifiedLevels(t *testing.T) { | ||
| buf := bytes.Buffer{} | ||
| levels := log.NewLevels( | ||
| log.NewJSONLogger(&buf), | ||
| log.LevelKey("l"), | ||
| log.DebugLevelValue("⛄"), | ||
| log.InfoLevelValue("🌜"), | ||
| log.ErrorLevelValue("🌊"), | ||
| ) | ||
| log.With(levels.Debug, "easter_island", "🗿").Log("msg", "💃💃💃") | ||
| if want, have := `{"easter_island":"🗿","l":"⛄","msg":"💃💃💃"}`+"\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package log | ||
|
|
||
| // Logger is the least-common-denominator interface for all log operations. | ||
| type Logger interface { | ||
| Log(keyvals ...interface{}) error | ||
| } | ||
|
|
||
| // With new, contextualized Logger with the passed keyvals already applied. | ||
| func With(logger Logger, keyvals ...interface{}) Logger { | ||
| if w, ok := logger.(Wither); ok { | ||
| return w.With(keyvals...) | ||
| } | ||
| return LoggerFunc(func(kvs ...interface{}) error { | ||
| return logger.Log(append(keyvals, kvs...)...) | ||
| }) | ||
| } | ||
|
|
||
| // LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If | ||
| // f is a function with the appropriate signature, LoggerFunc(f) is a Logger | ||
| // object that calls f. | ||
| type LoggerFunc func(...interface{}) error | ||
|
|
||
| // Log implements Logger by calling f(keyvals...). | ||
| func (f LoggerFunc) Log(keyvals ...interface{}) error { | ||
| return f(keyvals...) | ||
| } | ||
|
|
||
| // Wither describes an optimization that Logger implementations may make. If a | ||
| // Logger implements Wither, the package-level With function will invoke it | ||
| // when creating a new, contextual logger. | ||
| type Wither interface { | ||
| With(keyvals ...interface{}) Logger | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package log_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/peterbourgon/gokit/log" | ||
| ) | ||
|
|
||
| func TestWith(t *testing.T) { | ||
| buf := &bytes.Buffer{} | ||
| logger := log.NewJSONLogger(buf) | ||
| logger = log.With(logger, "a", 123) | ||
| logger = log.With(logger, "b", "c") // With should stack | ||
| if err := logger.Log("msg", "message"); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if want, have := `{"a":123,"b":"c","msg":"message"}`+"\n", buf.String(); want != have { | ||
| t.Errorf("want\n\t%#v, have\n\t%#v", want, have) | ||
| } | ||
| } | ||
|
|
||
| func TestWither(t *testing.T) { | ||
| logger := &mylogger{} | ||
| log.With(logger, "a", "b").Log("c", "d") | ||
| if want, have := 1, logger.withs; want != have { | ||
| t.Errorf("want %d, have %d", want, have) | ||
| } | ||
| } | ||
|
|
||
| type mylogger struct{ withs int } | ||
|
|
||
| func (l *mylogger) Log(keyvals ...interface{}) error { return nil } | ||
|
|
||
| func (l *mylogger) With(keyvals ...interface{}) log.Logger { l.withs++; return l } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package log | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| ) | ||
|
|
||
| type prefixLogger struct { | ||
| io.Writer | ||
| } | ||
|
|
||
| // NewPrefixLogger returns a basic logger that encodes keyvals as simple "k=v" | ||
| // pairs to the Writer. | ||
| func NewPrefixLogger(w io.Writer) Logger { | ||
| return &prefixLogger{w} | ||
| } | ||
|
|
||
| func (l prefixLogger) Log(keyvals ...interface{}) error { | ||
| if len(keyvals)%2 == 1 { | ||
| panic("odd number of keyvals") | ||
| } | ||
| for i := 0; i < len(keyvals); i += 2 { | ||
| if i != 0 { | ||
| if _, err := fmt.Fprint(l.Writer, " "); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if _, err := fmt.Fprintf(l.Writer, "%s=%v", keyvals[i], keyvals[i+1]); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if _, err := fmt.Fprintln(l.Writer); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package log_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "errors" | ||
| "io/ioutil" | ||
| "testing" | ||
|
|
||
| "github.com/peterbourgon/gokit/log" | ||
| ) | ||
|
|
||
| func TestPrefixLogger(t *testing.T) { | ||
| buf := &bytes.Buffer{} | ||
| logger := log.NewPrefixLogger(buf) | ||
|
|
||
| if err := logger.Log("hello", "world"); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if want, have := "hello=world\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
|
|
||
| buf.Reset() | ||
| if err := logger.Log("a", 1, "err", errors.New("error")); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if want, have := "a=1 err=error\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
|
|
||
| buf.Reset() | ||
| if err := logger.Log("std_map", map[int]int{1: 2}, "my_map", mymap{0: 0}); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if want, have := "std_map=map[1:2] my_map=special_behavior\n", buf.String(); want != have { | ||
| t.Errorf("want %#v, have %#v", want, have) | ||
| } | ||
| } | ||
|
|
||
| type mymap map[int]int | ||
|
|
||
| func (m mymap) String() string { return "special_behavior" } | ||
|
|
||
| func BenchmarkPrefixLoggerSimple(b *testing.B) { | ||
| benchmarkRunner(b, log.NewPrefixLogger(ioutil.Discard), baseMessage) | ||
| } | ||
|
|
||
| func BenchmarkPrefixLoggerContextual(b *testing.B) { | ||
| benchmarkRunner(b, log.NewPrefixLogger(ioutil.Discard), withMessage) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my last comment was lost with the change to varargs: if
vis of typeerror, it will be encoded as an empty{}by the JSON encoder. Ideally it should be special cased to call.Error()ifvis of typeerror.