From 53371e36641339bc393fe4e2d1fcf25d84bd0208 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 10 Mar 2014 19:22:08 -0400 Subject: [PATCH] Add features from README --- README.md | 9 +-- entry.go | 141 ++++++++++++++++------------------------------ examples/text.go | 36 ++++++++++++ examples_test.go | 63 --------------------- formatter.go | 5 ++ hooks.go | 28 +++++++++ json_formatter.go | 17 ++++++ logger.go | 40 +++++++++++-- logrus.go | 13 ++--- logrus_test.go | 52 +++++++++++++++++ text_formatter.go | 59 +++++++++++++++++++ 11 files changed, 285 insertions(+), 178 deletions(-) create mode 100644 examples/text.go delete mode 100644 examples_test.go create mode 100644 formatter.go create mode 100644 hooks.go create mode 100644 json_formatter.go create mode 100644 logrus_test.go create mode 100644 text_formatter.go diff --git a/README.md b/README.md index e56ca77..7d677a5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,7 @@ # Logrus -Logrus is a simple, opinionated logging package for Go which is completely API -compatible with the standard library logger. It has six logging levels: Debug, -Info, Warn, Error, Fatal and Panic. It supports custom logging formatters, and -ships with JSON and nicely formatted text by default. It encourages the use of -logging key value pairs for discoverability. Logrus allows you to add hooks to -logging events at different levels, for instance to notify an external error -tracker. +Logrus is a simple, opinionated structured logging package for Go which is +completely API compatible with the standard library logger. #### Fields diff --git a/entry.go b/entry.go index 16f8c1c..d79e345 100644 --- a/entry.go +++ b/entry.go @@ -2,17 +2,10 @@ package logrus import ( "bytes" - "encoding/json" - "errors" "fmt" "io" "os" - "sort" - "strings" "time" - - "github.com/burke/ttyutils" - "github.com/tobi/airbrake-go" ) type Entry struct { @@ -33,64 +26,23 @@ func miniTS() int { func NewEntry(logger *Logger) *Entry { return &Entry{ logger: logger, - // Default is three fields, give a little extra room. Shouldn't hurt the - // scale. + // Default is three fields, give a little extra room Data: make(Fields, 5), } } -// TODO: Other formats? func (entry *Entry) Reader() (*bytes.Buffer, error) { - var serialized []byte - var err error + serialized, err := entry.logger.Formatter.Format(entry) + return bytes.NewBuffer(serialized), err +} - if Environment == "production" { - serialized, err = json.Marshal(entry.Data) - if err != nil { - return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) - } - serialized = append(serialized, '\n') - } else { - levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] - levelColor := 34 - - if entry.Data["level"] == "warning" { - levelColor = 33 - } else if entry.Data["level"] == "fatal" || - entry.Data["level"] == "panic" { - levelColor = 31 - } - - if ttyutils.IsTerminal(os.Stdout.Fd()) { - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m[%04d] %-45s ", levelColor, levelText, miniTS(), entry.Data["msg"]))...) - } - - // TODO: Pretty-print more by coloring when stdout is a tty - // TODO: If this is a println, it'll do a newline and then closing quote. - keys := make([]string, 0) - for k, _ := range entry.Data { - if k != "level" && k != "time" && k != "msg" { - keys = append(keys, k) - } - } - sort.Strings(keys) - first := true - for _, k := range keys { - v := entry.Data[k] - if first { - first = false - } else { - serialized = append(serialized, ' ') - } - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m=%v", levelColor, k, v))...) - } - - // serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm)\x1b[0m", levelColor))...) - - serialized = append(serialized, '\n') +func (entry *Entry) String() (string, error) { + reader, err := entry.Reader() + if err != nil { + return "", err } - return bytes.NewBuffer(serialized), nil + return reader.String(), err } func (entry *Entry) WithField(key string, value interface{}) *Entry { @@ -102,15 +54,12 @@ func (entry *Entry) WithFields(fields Fields) *Entry { for key, value := range fields { entry.WithField(key, value) } - return entry } func (entry *Entry) log(level string, msg string) string { - // TODO: Is the default format output from String() the one we want? entry.Data["time"] = time.Now().String() entry.Data["level"] = level - // TODO: Is this the best name? entry.Data["msg"] = msg reader, err := entry.Reader() @@ -118,25 +67,12 @@ func (entry *Entry) log(level string, msg string) string { fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) } - if Environment != "development" { - // Send HTTP request in a goroutine in warning environment to not halt the - // main thread. It's sent before logging due to panic. - if level == "warning" { - // TODO: new() should spawn an airbrake goroutine and this should send to - // that channel. This prevent us from spawning hundreds of goroutines in a - // hot code path generating a warning. - go entry.airbrake(reader.String()) - } else if level == "fatal" || level == "panic" { - entry.airbrake(reader.String()) - } - } - entry.logger.mu.Lock() defer entry.logger.mu.Unlock() _, err = io.Copy(entry.logger.Out, reader) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) + fmt.Fprintf(os.Stderr, "Failed to write to log, %v", err) } return reader.String() @@ -145,30 +81,42 @@ func (entry *Entry) log(level string, msg string) string { func (entry *Entry) Debug(args ...interface{}) { if Level >= LevelDebug { entry.log("debug", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelDebug, entry) } } func (entry *Entry) Info(args ...interface{}) { if Level >= LevelInfo { entry.log("info", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelInfo, entry) } } func (entry *Entry) Print(args ...interface{}) { if Level >= LevelInfo { entry.log("info", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelInfo, entry) } } -func (entry *Entry) Warning(args ...interface{}) { - if Level >= LevelWarning { +func (entry *Entry) Warn(args ...interface{}) { + if Level >= LevelWarn { entry.log("warning", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelWarn, entry) + } +} + +func (entry *Entry) Error(args ...interface{}) { + if Level >= LevelError { + entry.log("error", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelError, entry) } } func (entry *Entry) Fatal(args ...interface{}) { if Level >= LevelFatal { entry.log("fatal", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelFatal, entry) } os.Exit(1) } @@ -176,6 +124,7 @@ func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) { if Level >= LevelPanic { msg := entry.log("panic", fmt.Sprint(args...)) + entry.logger.Hooks.Fire(LevelPanic, entry) panic(msg) } panic(fmt.Sprint(args...)) @@ -195,8 +144,16 @@ func (entry *Entry) Printf(format string, args ...interface{}) { entry.Print(fmt.Sprintf(format, args...)) } +func (entry *Entry) Warnf(format string, args ...interface{}) { + entry.Warn(fmt.Sprintf(format, args...)) +} + func (entry *Entry) Warningf(format string, args ...interface{}) { - entry.Warning(fmt.Sprintf(format, args...)) + entry.Warn(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Errorf(format string, args ...interface{}) { + entry.Print(fmt.Sprintf(format, args...)) } func (entry *Entry) Fatalf(format string, args ...interface{}) { @@ -210,35 +167,33 @@ func (entry *Entry) Panicf(format string, args ...interface{}) { // Entry Println family functions func (entry *Entry) Debugln(args ...interface{}) { - entry.Debug(fmt.Sprintln(args...)) + entry.Debug(fmt.Sprint(args...)) } func (entry *Entry) Infoln(args ...interface{}) { - entry.Info(fmt.Sprintln(args...)) + entry.Info(fmt.Sprint(args...)) } func (entry *Entry) Println(args ...interface{}) { - entry.Print(fmt.Sprintln(args...)) + entry.Print(fmt.Sprint(args...)) +} + +func (entry *Entry) Warnln(args ...interface{}) { + entry.Warn(fmt.Sprint(args...)) } func (entry *Entry) Warningln(args ...interface{}) { - entry.Warning(fmt.Sprintln(args...)) + entry.Warn(fmt.Sprint(args...)) +} + +func (entry *Entry) Errorln(args ...interface{}) { + entry.Error(fmt.Sprint(args...)) } func (entry *Entry) Fatalln(args ...interface{}) { - entry.Fatal(fmt.Sprintln(args...)) + entry.Fatal(fmt.Sprint(args...)) } func (entry *Entry) Panicln(args ...interface{}) { - entry.Panic(fmt.Sprintln(args...)) -} - -func (entry *Entry) airbrake(exception string) { - err := airbrake.Notify(errors.New(exception)) - if err != nil { - entry.logger.WithFields(Fields{ - "source": "airbrake", - "endpoint": airbrake.Endpoint, - }).Infof("Failed to send exception to Airbrake") - } + entry.Panic(fmt.Sprint(args...)) } diff --git a/examples/text.go b/examples/text.go new file mode 100644 index 0000000..d10ba24 --- /dev/null +++ b/examples/text.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/Sirupsen/logrus" +) + +func main() { + log := logrus.New() + + for { + log.WithFields(logrus.Fields{ + "animal": "walrus", + "size": "10", + }).Print("Hello WOrld!!") + + log.WithFields(logrus.Fields{ + "omg": true, + "number": 122, + }).Warn("There were some omgs") + + log.WithFields(logrus.Fields{ + "animal": "walrus", + "size": "10", + }).Print("Hello WOrld!!") + + log.WithFields(logrus.Fields{ + "animal": "walrus", + "size": "10", + }).Print("Hello WOrld!!") + + log.WithFields(logrus.Fields{ + "omg": true, + "number": 122, + }).Fatal("There were some omgs") + } +} diff --git a/examples_test.go b/examples_test.go deleted file mode 100644 index 8826258..0000000 --- a/examples_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package logrus - -import ( - "github.com/tobi/airbrake-go" -) - -func ExampleLogger_Info() { - logger := New() - logger.Info("Simple logging call, compatible with the standard logger") - // { - // "level": "info", - // "msg": "Simple logging call, compatible with the standard logger", - // "time": "2014-02-23 19:57:35.862271048 -0500 EST" - // } -} - -func ExampleLogger_Warning() { - logger := New() - - airbrake.Environment = "production" - airbrake.ApiKey = "valid" - airbrake.Endpoint = "https://exceptions.example.com/notifer_api/v2/notices" - - // This will send an exception with Airbrake now that it has been setup. - logger.Warning("Something failed: %s", "failure") - // { - // "level": "warning", - // "msg": "Something failed: failure", - // "time": "2014-02-23 19:57:35.862271048 -0500 EST" - // } -} - -func ExampleLogger_WithField() { - logger := New() - logger.WithField("source", "kafka").Infof("Connection to Kafka failed with %s", "some error") - // { - // "level": "info", - // "source": "kafka", - // "msg": "Connection to Kafka failed with some error", - // "time": "2014-02-23 19:57:35.862271048 -0500 EST" - // } -} - -func ExampleLogger_WithFields() { - logger := New() - logger.WithFields(Fields{ - "animal": "walrus", - "location": "New York Aquarium", - "weather": "rain", - "name": "Wally", - "event": "escape", - }).Info("Walrus has escaped the aquarium! Action required!") - // { - // "level": "info", - // "animal": "walrus", - // "location": "New York Aquarium", - // "weather":"rain", - // "name": "Wally", - // "event":"escape", - // "msg": "Walrus has escaped the aquarium! Action required!", - // "time": "2014-02-23 19:57:35.862271048 -0500 EST" - // } -} diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..a96f58a --- /dev/null +++ b/formatter.go @@ -0,0 +1,5 @@ +package logrus + +type Formatter interface { + Format(*Entry) ([]byte, error) +} diff --git a/hooks.go b/hooks.go new file mode 100644 index 0000000..fe5f032 --- /dev/null +++ b/hooks.go @@ -0,0 +1,28 @@ +package logrus + +type Hook interface { + Levels() []LevelType + Fire(*Entry) error +} + +type levelHooks map[LevelType][]Hook + +func (hooks levelHooks) Add(hook Hook) { + for _, level := range hook.Levels() { + if _, ok := hooks[level]; !ok { + hooks[level] = make([]Hook, 0, 1) + } + + hooks[level] = append(hooks[level], hook) + } +} + +func (hooks levelHooks) Fire(level LevelType, entry *Entry) error { + for _, hook := range hooks[level] { + if err := hook.Fire(entry); err != nil { + return err + } + } + + return nil +} diff --git a/json_formatter.go b/json_formatter.go new file mode 100644 index 0000000..cb3489e --- /dev/null +++ b/json_formatter.go @@ -0,0 +1,17 @@ +package logrus + +import ( + "encoding/json" + "fmt" +) + +type JSONFormatter struct { +} + +func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { + serialized, err := json.Marshal(entry.Data) + if err != nil { + return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) + } + return append(serialized, '\n'), nil +} diff --git a/logger.go b/logger.go index d7343d1..3379469 100644 --- a/logger.go +++ b/logger.go @@ -7,13 +7,17 @@ import ( ) type Logger struct { - Out io.Writer - mu sync.Mutex + Out io.Writer + Hooks levelHooks + Formatter Formatter + mu sync.Mutex } func New() *Logger { return &Logger{ - Out: os.Stdout, // Default to stdout, change it if you want. + Out: os.Stdout, // Default to stdout, change it if you want. + Formatter: new(TextFormatter), + Hooks: make(levelHooks), } } @@ -39,8 +43,16 @@ func (logger *Logger) Printf(format string, args ...interface{}) { NewEntry(logger).Printf(format, args...) } +func (logger *Logger) Warnf(format string, args ...interface{}) { + NewEntry(logger).Warnf(format, args...) +} + func (logger *Logger) Warningf(format string, args ...interface{}) { - NewEntry(logger).Warningf(format, args...) + NewEntry(logger).Warnf(format, args...) +} + +func (logger *Logger) Errorf(format string, args ...interface{}) { + NewEntry(logger).Errorf(format, args...) } func (logger *Logger) Fatalf(format string, args ...interface{}) { @@ -65,8 +77,16 @@ func (logger *Logger) Print(args ...interface{}) { NewEntry(logger).Print(args...) } +func (logger *Logger) Warn(args ...interface{}) { + NewEntry(logger).Warn(args...) +} + func (logger *Logger) Warning(args ...interface{}) { - NewEntry(logger).Warning(args...) + NewEntry(logger).Warn(args...) +} + +func (logger *Logger) Error(args ...interface{}) { + NewEntry(logger).Error(args...) } func (logger *Logger) Fatal(args ...interface{}) { @@ -91,8 +111,16 @@ func (logger *Logger) Println(args ...interface{}) { NewEntry(logger).Println(args...) } +func (logger *Logger) Warnln(args ...interface{}) { + NewEntry(logger).Warnln(args...) +} + func (logger *Logger) Warningln(args ...interface{}) { - NewEntry(logger).Warningln(args...) + NewEntry(logger).Warnln(args...) +} + +func (logger *Logger) Errorln(args ...interface{}) { + NewEntry(logger).Errorln(args...) } func (logger *Logger) Fatalln(args ...interface{}) { diff --git a/logrus.go b/logrus.go index fdce43c..bff336f 100644 --- a/logrus.go +++ b/logrus.go @@ -1,24 +1,19 @@ package logrus -import () - -// TODO: Type naming here feels awkward, but the exposed variable should be -// Level. That's more important than the type name, and libraries should be -// reaching for logrus.Level{Debug,Info,Warning,Fatal}, not defining the type -// themselves as an int. -type LevelType uint8 type Fields map[string]interface{} +type LevelType uint8 + const ( LevelPanic LevelType = iota LevelFatal - LevelWarning + LevelError + LevelWarn LevelInfo LevelDebug ) var Level LevelType = LevelInfo -var Environment string = "development" // StandardLogger is what your logrus-enabled library should take, that way // it'll accept a stdlib logger and a logrus logger. There's no standard diff --git a/logrus_test.go b/logrus_test.go new file mode 100644 index 0000000..64baa40 --- /dev/null +++ b/logrus_test.go @@ -0,0 +1,52 @@ +package logrus + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) { + var buffer bytes.Buffer + var fields Fields + + logger := New() + logger.Out = &buffer + logger.Formatter = new(JSONFormatter) + + log(logger) + + err := json.Unmarshal(buffer.Bytes(), &fields) + assert.Nil(t, err) + + assertions(fields) +} + +func TestPrint(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.Print("test") + }, func(fields Fields) { + assert.Equal(t, fields["msg"], "test") + assert.Equal(t, fields["level"], "info") + }) +} + +func TestInfo(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.Info("test") + }, func(fields Fields) { + assert.Equal(t, fields["msg"], "test") + assert.Equal(t, fields["level"], "info") + }) +} + +func TestWarn(t *testing.T) { + LogAndAssertJSON(t, func(log *Logger) { + log.Warn("test") + }, func(fields Fields) { + assert.Equal(t, fields["msg"], "test") + assert.Equal(t, fields["level"], "warning") + }) +} diff --git a/text_formatter.go b/text_formatter.go new file mode 100644 index 0000000..f6b9d14 --- /dev/null +++ b/text_formatter.go @@ -0,0 +1,59 @@ +package logrus + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/burke/ttyutils" +) + +const ( + nocolor = 0 + red = 31 + green = 32 + yellow = 33 + blue = 34 +) + +type TextFormatter struct { +} + +func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { + var serialized []byte + + levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] + levelColor := blue + + if entry.Data["level"] == "warning" { + levelColor = yellow + } else if entry.Data["level"] == "fatal" || + entry.Data["level"] == "panic" { + levelColor = red + } + + if ttyutils.IsTerminal(os.Stdout.Fd()) { + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m[%04d] %-45s ", levelColor, levelText, miniTS(), entry.Data["msg"]))...) + } + + keys := make([]string, 0) + for k, _ := range entry.Data { + if k != "level" && k != "time" && k != "msg" { + keys = append(keys, k) + } + } + sort.Strings(keys) + first := true + for _, k := range keys { + v := entry.Data[k] + if first { + first = false + } else { + serialized = append(serialized, ' ') + } + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m=%v", levelColor, k, v))...) + } + + return append(serialized, '\n'), nil +}