From 84ff01f44417050249ccc12cf9a69bfd55fe6e0c Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sun, 23 Feb 2014 09:57:04 -0500 Subject: [PATCH 01/18] Initial commit --- logrus.go | 353 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 logrus.go diff --git a/logrus.go b/logrus.go new file mode 100644 index 0000000..457343c --- /dev/null +++ b/logrus.go @@ -0,0 +1,353 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/tobi/airbrake-go" +) + +// 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{} + +const ( + LevelPanic LevelType = iota + LevelFatal + LevelWarning + 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 +// interface, this is the closest we get, unfortunately. +type StandardLogger interface { + Print(...interface{}) + Printf(string, ...interface{}) + Printfln(...interface{}) + + Fatal(...interface{}) + Fatalf(string, ...interface{}) + Fatalln(...interface{}) + + Panic(...interface{}) + Panicf(string, ...interface{}) + Panicln(...interface{}) +} + +type Logger struct { + Out io.Writer + mu sync.Mutex +} + +type Entry struct { + logger *Logger + Data Fields +} + +// TODO: Other formats? +func (entry *Entry) Reader() (read *bytes.Buffer, err error) { + var serialized []byte + + if Environment == "production" { + serialized, err = json.Marshal(entry.Data) + } else { + // TODO: Pretty-print more by coloring when stdout is a tty + serialized, err = json.MarshalIndent(entry.Data, "", " ") + } + + if err != nil { + return nil, err + } + + serialized = append(serialized, '\n') + + return bytes.NewBuffer(serialized), nil +} + +func New() *Logger { + environment := strings.ToLower(os.Getenv("ENV")) + if environment == "" { + environment = "development" + } + + if airbrake.Environment == "" { + airbrake.Environment = environment + } + + return &Logger{ + Out: os.Stdout, // Default to stdout, change it if you want. + } +} + +func NewEntry(logger *Logger) *Entry { + return &Entry{ + logger: logger, + // Default is three fields, give a little extra room. Shouldn't hurt the + // scale. + Data: make(Fields, 5), + } +} + +func (logger *Logger) WithField(key string, value interface{}) *Entry { + entry := NewEntry(logger) + entry.WithField(key, value) + return entry +} + +func (logger *Logger) WithFields(fields Fields) *Entry { + entry := NewEntry(logger) + entry.WithFields(fields) + return entry +} + +func (entry *Entry) WithField(key string, value interface{}) *Entry { + entry.Data[key] = value + return entry +} + +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) { + // TODO: Is the default format output from String() the one we want? + entry.Data["timestamp"] = time.Now().String() + entry.Data["level"] = level + // TODO: Is this the best name? + entry.Data["msg"] = msg + + reader, err := entry.Reader() + if err != nil { + entry.logger.Panicln("Failed to marshal JSON ", err.Error()) + } + + // 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()) + } + + if level == "panic" { + panic(reader.String()) + } else { + entry.logger.mu.Lock() + io.Copy(entry.logger.Out, reader) + entry.logger.mu.Unlock() + } +} + +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 Print family functions + +func (entry *Entry) Debug(args ...interface{}) { + if Level >= LevelDebug { + entry.log("debug", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Info(args ...interface{}) { + if Level >= LevelInfo { + entry.log("info", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Print(args ...interface{}) { + if Level >= LevelInfo { + entry.log("info", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Warning(args ...interface{}) { + if Level >= LevelWarning { + entry.log("warning", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Fatal(args ...interface{}) { + if Level >= LevelFatal { + entry.log("fatal", fmt.Sprint(args...)) + } + os.Exit(1) +} + +func (entry *Entry) Panic(args ...interface{}) { + if Level >= LevelPanic { + entry.log("panic", fmt.Sprint(args...)) + } +} + +// Entry Printf family functions + +func (entry *Entry) Debugf(format string, args ...interface{}) { + entry.Debug(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Infof(format string, args ...interface{}) { + entry.Info(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Printf(format string, args ...interface{}) { + entry.Print(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Warningf(format string, args ...interface{}) { + entry.Warning(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Fatalf(format string, args ...interface{}) { + entry.Fatal(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Panicf(format string, args ...interface{}) { + entry.Panic(fmt.Sprintf(format, args...)) +} + +// Entry Println family functions + +func (entry *Entry) Debugln(args ...interface{}) { + entry.Debug(fmt.Sprintln(args...)) +} + +func (entry *Entry) Infoln(args ...interface{}) { + entry.Info(fmt.Sprintln(args...)) +} + +func (entry *Entry) Println(args ...interface{}) { + entry.Print(fmt.Sprintln(args...)) +} + +func (entry *Entry) Warningln(args ...interface{}) { + entry.Warning(fmt.Sprintln(args...)) +} + +func (entry *Entry) Fatalln(args ...interface{}) { + entry.Fatal(fmt.Sprintln(args...)) +} + +func (entry *Entry) Panicln(args ...interface{}) { + entry.Panic(fmt.Sprintln(args...)) +} + +// Logger Printf family functions + +func (logger *Logger) Debugf(format string, args ...interface{}) { + NewEntry(logger).Debugf(format, args...) +} + +func (logger *Logger) Infof(format string, args ...interface{}) { + NewEntry(logger).Infof(format, args...) +} + +func (logger *Logger) Printf(format string, args ...interface{}) { + NewEntry(logger).Printf(format, args...) +} + +func (logger *Logger) Warningf(format string, args ...interface{}) { + NewEntry(logger).Warningf(format, args...) +} + +func (logger *Logger) Fatalf(format string, args ...interface{}) { + NewEntry(logger).Fatalf(format, args...) +} + +func (logger *Logger) Panicf(format string, args ...interface{}) { + NewEntry(logger).Panicf(format, args...) +} + +// Logger Print family functions + +func (logger *Logger) Debug(args ...interface{}) { + NewEntry(logger).Debug(args...) +} + +func (logger *Logger) Info(args ...interface{}) { + NewEntry(logger).Info(args...) +} + +func (logger *Logger) Print(args ...interface{}) { + NewEntry(logger).Print(args...) +} + +func (logger *Logger) Warning(args ...interface{}) { + NewEntry(logger).Warning(args...) +} + +func (logger *Logger) Fatal(args ...interface{}) { + NewEntry(logger).Fatal(args...) +} + +func (logger *Logger) Panic(args ...interface{}) { + NewEntry(logger).Panic(args...) +} + +// Logger Println family functions + +func (logger *Logger) Debugln(args ...interface{}) { + NewEntry(logger).Debugln(args...) +} + +func (logger *Logger) Infoln(args ...interface{}) { + NewEntry(logger).Infoln(args...) +} + +func (logger *Logger) Println(args ...interface{}) { + NewEntry(logger).Println(args...) +} + +func (logger *Logger) Warningln(args ...interface{}) { + NewEntry(logger).Warningln(args...) +} + +func (logger *Logger) Fatalln(args ...interface{}) { + NewEntry(logger).Fatalln(args...) +} + +func (logger *Logger) Panicln(args ...interface{}) { + NewEntry(logger).Panicln(args...) +} + +// TODO: Print, Fatal, etc. + +func main() { + Environment = "development" + Level = LevelDebug + logger := New() + logger.WithField("animal", "walrus").WithField("value", 10).Infof("OMG HELLO") + logger.Infof("lolsup") + logger.Debugf("why brackets?") + logger.Debug("lolsup") + logger.Fatalf("omg") +} From 6c895096e850ba3210087d435d3867a0d4c4a68f Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sun, 23 Feb 2014 19:50:42 -0500 Subject: [PATCH 02/18] split into multiple files --- entry.go | 191 ++++++++++++++++++++++++++++++++ logger.go | 121 +++++++++++++++++++++ logrus.go | 319 +----------------------------------------------------- 3 files changed, 314 insertions(+), 317 deletions(-) create mode 100644 entry.go create mode 100644 logger.go diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..a492078 --- /dev/null +++ b/entry.go @@ -0,0 +1,191 @@ +package logrus + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/tobi/airbrake-go" +) + +type Entry struct { + logger *Logger + Data Fields +} + +func NewEntry(logger *Logger) *Entry { + return &Entry{ + logger: logger, + // Default is three fields, give a little extra room. Shouldn't hurt the + // scale. + Data: make(Fields, 5), + } +} + +// TODO: Other formats? +func (entry *Entry) Reader() (read *bytes.Buffer, err error) { + var serialized []byte + + if Environment == "production" { + serialized, err = json.Marshal(entry.Data) + } else { + // TODO: Pretty-print more by coloring when stdout is a tty + serialized, err = json.MarshalIndent(entry.Data, "", " ") + } + + if err != nil { + return nil, err + } + + serialized = append(serialized, '\n') + + return bytes.NewBuffer(serialized), nil +} + +func (entry *Entry) WithField(key string, value interface{}) *Entry { + entry.Data[key] = value + return entry +} + +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) { + // TODO: Is the default format output from String() the one we want? + entry.Data["timestamp"] = time.Now().String() + entry.Data["level"] = level + // TODO: Is this the best name? + entry.Data["msg"] = msg + + reader, err := entry.Reader() + if err != nil { + entry.logger.Panicln("Failed to marshal JSON ", err.Error()) + } + + // 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()) + } + + if level == "panic" { + panic(reader.String()) + } else { + entry.logger.mu.Lock() + io.Copy(entry.logger.Out, reader) + entry.logger.mu.Unlock() + } +} + +func (entry *Entry) Debug(args ...interface{}) { + if Level >= LevelDebug { + entry.log("debug", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Info(args ...interface{}) { + if Level >= LevelInfo { + entry.log("info", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Print(args ...interface{}) { + if Level >= LevelInfo { + entry.log("info", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Warning(args ...interface{}) { + if Level >= LevelWarning { + entry.log("warning", fmt.Sprint(args...)) + } +} + +func (entry *Entry) Fatal(args ...interface{}) { + if Level >= LevelFatal { + entry.log("fatal", fmt.Sprint(args...)) + } + os.Exit(1) +} + +func (entry *Entry) Panic(args ...interface{}) { + if Level >= LevelPanic { + entry.log("panic", fmt.Sprint(args...)) + } +} + +// Entry Printf family functions + +func (entry *Entry) Debugf(format string, args ...interface{}) { + entry.Debug(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Infof(format string, args ...interface{}) { + entry.Info(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Printf(format string, args ...interface{}) { + entry.Print(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Warningf(format string, args ...interface{}) { + entry.Warning(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Fatalf(format string, args ...interface{}) { + entry.Fatal(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Panicf(format string, args ...interface{}) { + entry.Panic(fmt.Sprintf(format, args...)) +} + +// Entry Println family functions + +func (entry *Entry) Debugln(args ...interface{}) { + entry.Debug(fmt.Sprintln(args...)) +} + +func (entry *Entry) Infoln(args ...interface{}) { + entry.Info(fmt.Sprintln(args...)) +} + +func (entry *Entry) Println(args ...interface{}) { + entry.Print(fmt.Sprintln(args...)) +} + +func (entry *Entry) Warningln(args ...interface{}) { + entry.Warning(fmt.Sprintln(args...)) +} + +func (entry *Entry) Fatalln(args ...interface{}) { + entry.Fatal(fmt.Sprintln(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") + } +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..cf3154d --- /dev/null +++ b/logger.go @@ -0,0 +1,121 @@ +package logrus + +import ( + "io" + "os" + "strings" + "sync" + + "github.com/tobi/airbrake-go" +) + +type Logger struct { + Out io.Writer + mu sync.Mutex +} + +func New() *Logger { + environment := strings.ToLower(os.Getenv("ENV")) + if environment == "" { + environment = "development" + } + + if airbrake.Environment == "" { + airbrake.Environment = environment + } + + return &Logger{ + Out: os.Stdout, // Default to stdout, change it if you want. + } +} + +func (logger *Logger) WithField(key string, value interface{}) *Entry { + entry := NewEntry(logger) + entry.WithField(key, value) + return entry +} + +func (logger *Logger) WithFields(fields Fields) *Entry { + entry := NewEntry(logger) + entry.WithFields(fields) + return entry +} + +// Entry Print family functions +// Logger Printf family functions + +func (logger *Logger) Debugf(format string, args ...interface{}) { + NewEntry(logger).Debugf(format, args...) +} + +func (logger *Logger) Infof(format string, args ...interface{}) { + NewEntry(logger).Infof(format, args...) +} + +func (logger *Logger) Printf(format string, args ...interface{}) { + NewEntry(logger).Printf(format, args...) +} + +func (logger *Logger) Warningf(format string, args ...interface{}) { + NewEntry(logger).Warningf(format, args...) +} + +func (logger *Logger) Fatalf(format string, args ...interface{}) { + NewEntry(logger).Fatalf(format, args...) +} + +func (logger *Logger) Panicf(format string, args ...interface{}) { + NewEntry(logger).Panicf(format, args...) +} + +// Logger Print family functions + +func (logger *Logger) Debug(args ...interface{}) { + NewEntry(logger).Debug(args...) +} + +func (logger *Logger) Info(args ...interface{}) { + NewEntry(logger).Info(args...) +} + +func (logger *Logger) Print(args ...interface{}) { + NewEntry(logger).Print(args...) +} + +func (logger *Logger) Warning(args ...interface{}) { + NewEntry(logger).Warning(args...) +} + +func (logger *Logger) Fatal(args ...interface{}) { + NewEntry(logger).Fatal(args...) +} + +func (logger *Logger) Panic(args ...interface{}) { + NewEntry(logger).Panic(args...) +} + +// Logger Println family functions + +func (logger *Logger) Debugln(args ...interface{}) { + NewEntry(logger).Debugln(args...) +} + +func (logger *Logger) Infoln(args ...interface{}) { + NewEntry(logger).Infoln(args...) +} + +func (logger *Logger) Println(args ...interface{}) { + NewEntry(logger).Println(args...) +} + +func (logger *Logger) Warningln(args ...interface{}) { + NewEntry(logger).Warningln(args...) +} + +func (logger *Logger) Fatalln(args ...interface{}) { + NewEntry(logger).Fatalln(args...) +} + +func (logger *Logger) Panicln(args ...interface{}) { + NewEntry(logger).Panicln(args...) +} diff --git a/logrus.go b/logrus.go index 457343c..fdce43c 100644 --- a/logrus.go +++ b/logrus.go @@ -1,18 +1,6 @@ -package main +package logrus -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "strings" - "sync" - "time" - - "github.com/tobi/airbrake-go" -) +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 @@ -48,306 +36,3 @@ type StandardLogger interface { Panicf(string, ...interface{}) Panicln(...interface{}) } - -type Logger struct { - Out io.Writer - mu sync.Mutex -} - -type Entry struct { - logger *Logger - Data Fields -} - -// TODO: Other formats? -func (entry *Entry) Reader() (read *bytes.Buffer, err error) { - var serialized []byte - - if Environment == "production" { - serialized, err = json.Marshal(entry.Data) - } else { - // TODO: Pretty-print more by coloring when stdout is a tty - serialized, err = json.MarshalIndent(entry.Data, "", " ") - } - - if err != nil { - return nil, err - } - - serialized = append(serialized, '\n') - - return bytes.NewBuffer(serialized), nil -} - -func New() *Logger { - environment := strings.ToLower(os.Getenv("ENV")) - if environment == "" { - environment = "development" - } - - if airbrake.Environment == "" { - airbrake.Environment = environment - } - - return &Logger{ - Out: os.Stdout, // Default to stdout, change it if you want. - } -} - -func NewEntry(logger *Logger) *Entry { - return &Entry{ - logger: logger, - // Default is three fields, give a little extra room. Shouldn't hurt the - // scale. - Data: make(Fields, 5), - } -} - -func (logger *Logger) WithField(key string, value interface{}) *Entry { - entry := NewEntry(logger) - entry.WithField(key, value) - return entry -} - -func (logger *Logger) WithFields(fields Fields) *Entry { - entry := NewEntry(logger) - entry.WithFields(fields) - return entry -} - -func (entry *Entry) WithField(key string, value interface{}) *Entry { - entry.Data[key] = value - return entry -} - -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) { - // TODO: Is the default format output from String() the one we want? - entry.Data["timestamp"] = time.Now().String() - entry.Data["level"] = level - // TODO: Is this the best name? - entry.Data["msg"] = msg - - reader, err := entry.Reader() - if err != nil { - entry.logger.Panicln("Failed to marshal JSON ", err.Error()) - } - - // 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()) - } - - if level == "panic" { - panic(reader.String()) - } else { - entry.logger.mu.Lock() - io.Copy(entry.logger.Out, reader) - entry.logger.mu.Unlock() - } -} - -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 Print family functions - -func (entry *Entry) Debug(args ...interface{}) { - if Level >= LevelDebug { - entry.log("debug", fmt.Sprint(args...)) - } -} - -func (entry *Entry) Info(args ...interface{}) { - if Level >= LevelInfo { - entry.log("info", fmt.Sprint(args...)) - } -} - -func (entry *Entry) Print(args ...interface{}) { - if Level >= LevelInfo { - entry.log("info", fmt.Sprint(args...)) - } -} - -func (entry *Entry) Warning(args ...interface{}) { - if Level >= LevelWarning { - entry.log("warning", fmt.Sprint(args...)) - } -} - -func (entry *Entry) Fatal(args ...interface{}) { - if Level >= LevelFatal { - entry.log("fatal", fmt.Sprint(args...)) - } - os.Exit(1) -} - -func (entry *Entry) Panic(args ...interface{}) { - if Level >= LevelPanic { - entry.log("panic", fmt.Sprint(args...)) - } -} - -// Entry Printf family functions - -func (entry *Entry) Debugf(format string, args ...interface{}) { - entry.Debug(fmt.Sprintf(format, args...)) -} - -func (entry *Entry) Infof(format string, args ...interface{}) { - entry.Info(fmt.Sprintf(format, args...)) -} - -func (entry *Entry) Printf(format string, args ...interface{}) { - entry.Print(fmt.Sprintf(format, args...)) -} - -func (entry *Entry) Warningf(format string, args ...interface{}) { - entry.Warning(fmt.Sprintf(format, args...)) -} - -func (entry *Entry) Fatalf(format string, args ...interface{}) { - entry.Fatal(fmt.Sprintf(format, args...)) -} - -func (entry *Entry) Panicf(format string, args ...interface{}) { - entry.Panic(fmt.Sprintf(format, args...)) -} - -// Entry Println family functions - -func (entry *Entry) Debugln(args ...interface{}) { - entry.Debug(fmt.Sprintln(args...)) -} - -func (entry *Entry) Infoln(args ...interface{}) { - entry.Info(fmt.Sprintln(args...)) -} - -func (entry *Entry) Println(args ...interface{}) { - entry.Print(fmt.Sprintln(args...)) -} - -func (entry *Entry) Warningln(args ...interface{}) { - entry.Warning(fmt.Sprintln(args...)) -} - -func (entry *Entry) Fatalln(args ...interface{}) { - entry.Fatal(fmt.Sprintln(args...)) -} - -func (entry *Entry) Panicln(args ...interface{}) { - entry.Panic(fmt.Sprintln(args...)) -} - -// Logger Printf family functions - -func (logger *Logger) Debugf(format string, args ...interface{}) { - NewEntry(logger).Debugf(format, args...) -} - -func (logger *Logger) Infof(format string, args ...interface{}) { - NewEntry(logger).Infof(format, args...) -} - -func (logger *Logger) Printf(format string, args ...interface{}) { - NewEntry(logger).Printf(format, args...) -} - -func (logger *Logger) Warningf(format string, args ...interface{}) { - NewEntry(logger).Warningf(format, args...) -} - -func (logger *Logger) Fatalf(format string, args ...interface{}) { - NewEntry(logger).Fatalf(format, args...) -} - -func (logger *Logger) Panicf(format string, args ...interface{}) { - NewEntry(logger).Panicf(format, args...) -} - -// Logger Print family functions - -func (logger *Logger) Debug(args ...interface{}) { - NewEntry(logger).Debug(args...) -} - -func (logger *Logger) Info(args ...interface{}) { - NewEntry(logger).Info(args...) -} - -func (logger *Logger) Print(args ...interface{}) { - NewEntry(logger).Print(args...) -} - -func (logger *Logger) Warning(args ...interface{}) { - NewEntry(logger).Warning(args...) -} - -func (logger *Logger) Fatal(args ...interface{}) { - NewEntry(logger).Fatal(args...) -} - -func (logger *Logger) Panic(args ...interface{}) { - NewEntry(logger).Panic(args...) -} - -// Logger Println family functions - -func (logger *Logger) Debugln(args ...interface{}) { - NewEntry(logger).Debugln(args...) -} - -func (logger *Logger) Infoln(args ...interface{}) { - NewEntry(logger).Infoln(args...) -} - -func (logger *Logger) Println(args ...interface{}) { - NewEntry(logger).Println(args...) -} - -func (logger *Logger) Warningln(args ...interface{}) { - NewEntry(logger).Warningln(args...) -} - -func (logger *Logger) Fatalln(args ...interface{}) { - NewEntry(logger).Fatalln(args...) -} - -func (logger *Logger) Panicln(args ...interface{}) { - NewEntry(logger).Panicln(args...) -} - -// TODO: Print, Fatal, etc. - -func main() { - Environment = "development" - Level = LevelDebug - logger := New() - logger.WithField("animal", "walrus").WithField("value", 10).Infof("OMG HELLO") - logger.Infof("lolsup") - logger.Debugf("why brackets?") - logger.Debug("lolsup") - logger.Fatalf("omg") -} From 0bd36d372c159acc5f9eba775d7c1932cf8bc638 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sun, 23 Feb 2014 19:53:50 -0500 Subject: [PATCH 03/18] code cleanup --- entry.go | 13 +++++++++---- logger.go | 20 ++------------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/entry.go b/entry.go index a492078..5e349a3 100644 --- a/entry.go +++ b/entry.go @@ -61,14 +61,15 @@ func (entry *Entry) WithFields(fields Fields) *Entry { func (entry *Entry) log(level string, msg string) { // TODO: Is the default format output from String() the one we want? - entry.Data["timestamp"] = time.Now().String() + entry.Data["time"] = time.Now().String() entry.Data["level"] = level // TODO: Is this the best name? entry.Data["msg"] = msg reader, err := entry.Reader() if err != nil { - entry.logger.Panicln("Failed to marshal JSON ", err.Error()) + // TODO: Panic? + entry.logger.Panicln("Failed to marshal JSON: ", err.Error()) } // Send HTTP request in a goroutine in warning environment to not halt the @@ -86,8 +87,12 @@ func (entry *Entry) log(level string, msg string) { panic(reader.String()) } else { entry.logger.mu.Lock() - io.Copy(entry.logger.Out, reader) - entry.logger.mu.Unlock() + defer entry.logger.mu.Unlock() + _, err := io.Copy(entry.logger.Out, reader) + // TODO: Panic? + if err != nil { + entry.logger.Panicln("Failed to log message: ", err.Error()) + } } } diff --git a/logger.go b/logger.go index cf3154d..6a9ce5c 100644 --- a/logger.go +++ b/logger.go @@ -3,10 +3,7 @@ package logrus import ( "io" "os" - "strings" "sync" - - "github.com/tobi/airbrake-go" ) type Logger struct { @@ -15,30 +12,17 @@ type Logger struct { } func New() *Logger { - environment := strings.ToLower(os.Getenv("ENV")) - if environment == "" { - environment = "development" - } - - if airbrake.Environment == "" { - airbrake.Environment = environment - } - return &Logger{ Out: os.Stdout, // Default to stdout, change it if you want. } } func (logger *Logger) WithField(key string, value interface{}) *Entry { - entry := NewEntry(logger) - entry.WithField(key, value) - return entry + return NewEntry(logger).WithField(key, value) } func (logger *Logger) WithFields(fields Fields) *Entry { - entry := NewEntry(logger) - entry.WithFields(fields) - return entry + return NewEntry(logger).WithFields(fields) } // Entry Print family functions From 520486e2442168155a73b28e0803a0f9a9f37285 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sun, 23 Feb 2014 20:07:09 -0500 Subject: [PATCH 04/18] Add examples --- examples_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples_test.go diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..f5e30d3 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,63 @@ +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" + // } +} From b6f53cb91ba959dc42b6659fd843c41b70d47bb6 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sun, 23 Feb 2014 20:19:34 -0500 Subject: [PATCH 05/18] README: rewrite --- README.md | 108 +++++++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index cad2676..1449967 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,75 @@ # Logrus -Logrus is a simple, opinionated logging package for Go. It has three debugging -levels: +Logrus is a simple, opinionated logging package for Go. Features include: -* `LevelDebug`: Debugging, usually turned off for deploys. -* `LevelInfo`: Info, useful for monitoring in production. -* `LevelWarning`: Warnings that should definitely be noted. These are sent to - `airbrake`. -* `LevelFatal`: Fatal messages that causes the application to crash. These are - sent to `airbrake`. +* **Level logging**. Logrus has the levels: Debug, Info, Warning and Fatal. +* **Exceptions**. Warnings will log as an exception along with logging it to + out, without quitting. Fatal will do the same, but call `os.Exit(1)` after + emitting the exception. +* **JSON**. Logrus currently logs as JSON by default. -## Usage +The API is completely compatible with the Go standard lib logger, with only the +features above added. -The global logging level is set by: `logrus.Level = logrus.{LevelDebug,LevelWarning,LevelFatal}`. +## Motivation -Note that for `airbrake` to work, `airbrake.Endpoint` and `airbrake.ApiKey` -should be set. - -There is a global logger, which new loggers inherit their settings from when -created (see example below), such as the place to redirect output. Logging can -be done with the global logging module: +The motivation for this library came out of a pattern seen in Go applications me +and others have been writing with functions such as: ```go -logrus.Debug("Something debugworthy happened: %s", importantStuff) -logrus.Info("Something infoworthy happened: %s", importantStuff) +func reportFatalError(err error) { + airbrake.Notify(err) + log.Fatal(err) +} -logrus.Warning("Something bad happened: %s", importantStuff) -// Reports to Airbrake - -logrus.Fatal("Something fatal happened: %s", importantStuff) -// Reports to Airbrake -// Then exits +func reportWarning(err error) { + airbrake.Notify(err) +} ``` -Types are encouraged to include their own logging object. This allows to set a -context dependent prefix to know where a certain message is coming from, without -cluttering every single message with this. +JSON logging is excellent for parsing logs for analysis and troubleshooting. +It's supported natively by log aggregators such as logstash and Splunk. Logging +JSON with logrus with the `WithFields` and `WithField` API in logrus forces you +to think about what context to log, to provide valuable troubleshoot information +later. + +## Example ```go -type Walrus struct { - TuskSize uint64 - Sex bool - logger logrus.Logger -} +import ( + "github.com/Sirupsen/logrus" +) -func NewWalrus(tuskSize uint64, sex bool) *Walrus { - return &Walrus{ - TuskSize: tuskSize, - Sex: bool, - logger: logrus.NewLogger("Walrus"), - } -} +var logger logrus.New() +func main() { + 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" + // } -func (walrus *Walrus) Mate(partner *Walrus) error { - if walrus.Sex == partner.Sex { - return errors.New("Incompatible mating partner.") - } - - walrus.logger.Info("Walrus with tusk sizes %d and %d are mating!", walrus.TuskSize, partner.TuskSize) - // Generates a logging message: [Info] [Walrus] Walrus with tusk sizes and are mating! - - // Walrus mating happens here - - return nil + 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" + // } } ``` + +Using `Warning` and `Fatal` to log to `airbrake` requires setting +`airbrake.Endpoint` and `airbrake.ApiKey`. See +[tobi/airbrake-go](https://github.com/tobi/airbrake-go). From 11dbaff3525030bc8eba6c76b11142f905235786 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 24 Feb 2014 06:34:12 -0500 Subject: [PATCH 06/18] Code review fixes --- entry.go | 32 +++++++++++++++++--------------- logger.go | 1 - 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/entry.go b/entry.go index 5e349a3..7f7925b 100644 --- a/entry.go +++ b/entry.go @@ -27,8 +27,9 @@ func NewEntry(logger *Logger) *Entry { } // TODO: Other formats? -func (entry *Entry) Reader() (read *bytes.Buffer, err error) { +func (entry *Entry) Reader() (*bytes.Buffer, error) { var serialized []byte + var err error if Environment == "production" { serialized, err = json.Marshal(entry.Data) @@ -38,7 +39,7 @@ func (entry *Entry) Reader() (read *bytes.Buffer, err error) { } if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) } serialized = append(serialized, '\n') @@ -59,7 +60,7 @@ func (entry *Entry) WithFields(fields Fields) *Entry { return entry } -func (entry *Entry) log(level string, msg string) { +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 @@ -69,7 +70,7 @@ func (entry *Entry) log(level string, msg string) { reader, err := entry.Reader() if err != nil { // TODO: Panic? - entry.logger.Panicln("Failed to marshal JSON: ", err.Error()) + entry.logger.Panicf("Failed to obtain reader, %v", err) } // Send HTTP request in a goroutine in warning environment to not halt the @@ -83,17 +84,16 @@ func (entry *Entry) log(level string, msg string) { entry.airbrake(reader.String()) } - if level == "panic" { - panic(reader.String()) - } else { - entry.logger.mu.Lock() - defer entry.logger.mu.Unlock() - _, err := io.Copy(entry.logger.Out, reader) - // TODO: Panic? - if err != nil { - entry.logger.Panicln("Failed to log message: ", err.Error()) - } + entry.logger.mu.Lock() + defer entry.logger.mu.Unlock() + + _, err = io.Copy(entry.logger.Out, reader) + // TODO: Panic? + if err != nil { + entry.logger.Panicln("Failed to log message, %v", err) } + + return reader.String() } func (entry *Entry) Debug(args ...interface{}) { @@ -129,8 +129,10 @@ func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) { if Level >= LevelPanic { - entry.log("panic", fmt.Sprint(args...)) + msg = entry.log("panic", fmt.Sprint(args...)) + panic(msg) } + panic(fmt.Sprint(args...)) } // Entry Printf family functions diff --git a/logger.go b/logger.go index 6a9ce5c..d7343d1 100644 --- a/logger.go +++ b/logger.go @@ -25,7 +25,6 @@ func (logger *Logger) WithFields(fields Fields) *Entry { return NewEntry(logger).WithFields(fields) } -// Entry Print family functions // Logger Printf family functions func (logger *Logger) Debugf(format string, args ...interface{}) { From 0b5ff9bceffcad72a317110281aa1bf827cd25ed Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 24 Feb 2014 06:35:56 -0500 Subject: [PATCH 07/18] Fix examples formatting --- examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index f5e30d3..8826258 100644 --- a/examples_test.go +++ b/examples_test.go @@ -57,7 +57,7 @@ func ExampleLogger_WithFields() { // "weather":"rain", // "name": "Wally", // "event":"escape", - // "msg": "Walrus has escaped the aquarium! Action required!") + // "msg": "Walrus has escaped the aquarium! Action required!", // "time": "2014-02-23 19:57:35.862271048 -0500 EST" // } } From b7027167d50f2642d28cb6e6f0b6e2a48d3c3cae Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 3 Mar 2014 17:33:35 -0500 Subject: [PATCH 08/18] entry: add basic coloring if tty is a terminal --- entry.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/entry.go b/entry.go index 7f7925b..da02aec 100644 --- a/entry.go +++ b/entry.go @@ -7,8 +7,10 @@ import ( "fmt" "io" "os" + "strings" "time" + "github.com/burke/ttyutils" "github.com/tobi/airbrake-go" ) @@ -33,16 +35,27 @@ func (entry *Entry) Reader() (*bytes.Buffer, error) { 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 { + if ttyutils.IsTerminal(os.Stdout.Fd()) { + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s: ", strings.ToUpper(entry.Data["level"].(string))))...) + } + // TODO: Pretty-print more by coloring when stdout is a tty - serialized, err = json.MarshalIndent(entry.Data, "", " ") - } + // TODO: If this is a println, it'll do a newline and then closing quote. + for k, v := range entry.Data { + serialized = append(serialized, []byte(fmt.Sprintf("%s='%s' ", k, v))...) + } - if err != nil { - return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) - } + if ttyutils.IsTerminal(os.Stdout.Fd()) { + serialized = append(serialized, []byte("\x1b[0m")...) + } - serialized = append(serialized, '\n') + serialized = append(serialized, '\n') + } return bytes.NewBuffer(serialized), nil } @@ -129,7 +142,7 @@ func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) { if Level >= LevelPanic { - msg = entry.log("panic", fmt.Sprint(args...)) + msg := entry.log("panic", fmt.Sprint(args...)) panic(msg) } panic(fmt.Sprint(args...)) From 3ef01c6291b5585a44ecd92c55977b96932f5056 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 3 Mar 2014 17:35:32 -0500 Subject: [PATCH 09/18] entry: don't panic --- entry.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/entry.go b/entry.go index da02aec..b326889 100644 --- a/entry.go +++ b/entry.go @@ -82,8 +82,7 @@ func (entry *Entry) log(level string, msg string) string { reader, err := entry.Reader() if err != nil { - // TODO: Panic? - entry.logger.Panicf("Failed to obtain reader, %v", err) + fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) } // Send HTTP request in a goroutine in warning environment to not halt the @@ -101,9 +100,8 @@ func (entry *Entry) log(level string, msg string) string { defer entry.logger.mu.Unlock() _, err = io.Copy(entry.logger.Out, reader) - // TODO: Panic? if err != nil { - entry.logger.Panicln("Failed to log message, %v", err) + fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) } return reader.String() From ce9bacf7b3f0e248e2fb45d3cdeee23d6aa0ff44 Mon Sep 17 00:00:00 2001 From: Burke Libbey Date: Tue, 4 Mar 2014 11:38:21 -0500 Subject: [PATCH 10/18] quickhax --- entry.go | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/entry.go b/entry.go index b326889..59572d6 100644 --- a/entry.go +++ b/entry.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "sort" "strings" "time" @@ -19,6 +20,16 @@ type Entry struct { Data Fields } +var baseTimestamp time.Time + +func init() { + baseTimestamp = time.Now() +} + +func miniTS() int { + return int(time.Since(baseTimestamp) / time.Second) +} + func NewEntry(logger *Logger) *Entry { return &Entry{ logger: logger, @@ -40,19 +51,30 @@ func (entry *Entry) Reader() (*bytes.Buffer, error) { } serialized = append(serialized, '\n') } else { + levelText := strings.ToUpper(entry.Data["level"].(string)) + levelColor := 34 + if levelText != "INFO" { + levelColor = 31 + } if ttyutils.IsTerminal(os.Stdout.Fd()) { - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s: ", strings.ToUpper(entry.Data["level"].(string))))...) + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m[%04d] %-45s \x1b[%dm(\x1b[0m", levelColor, levelText, miniTS(), entry.Data["msg"], levelColor))...) } // 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. - for k, v := range entry.Data { - serialized = append(serialized, []byte(fmt.Sprintf("%s='%s' ", k, v))...) + keys := make([]string, 0) + for k, _ := range entry.Data { + if k != "level" && k != "time" && k != "msg" { + keys = append(keys, k) + } + } + sort.Strings(keys) + for _, k := range keys { + v := entry.Data[k] + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s\x1b[0m='%s' ", k, v))...) } - if ttyutils.IsTerminal(os.Stdout.Fd()) { - serialized = append(serialized, []byte("\x1b[0m")...) - } + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm)\x1b[0m", levelColor))...) serialized = append(serialized, '\n') } From e3978aa87cdcb3c46a7e146f7c042e07f2bf9f5e Mon Sep 17 00:00:00 2001 From: Burke Libbey Date: Tue, 4 Mar 2014 11:44:09 -0500 Subject: [PATCH 11/18] morehax --- entry.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/entry.go b/entry.go index 59572d6..b9e0b21 100644 --- a/entry.go +++ b/entry.go @@ -69,9 +69,15 @@ func (entry *Entry) Reader() (*bytes.Buffer, error) { } } sort.Strings(keys) + first := true for _, k := range keys { v := entry.Data[k] - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s\x1b[0m='%s' ", k, v))...) + if first { + first = false + } else { + serialized = append(serialized, ' ') + } + serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s\x1b[0m=%v", k, v))...) } serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm)\x1b[0m", levelColor))...) From f803b61ca1c6614cb0b8b6a771ed5624d49391f2 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Thu, 6 Mar 2014 09:56:52 -0500 Subject: [PATCH 12/18] entry: keys colored by severity --- entry.go | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/entry.go b/entry.go index b9e0b21..16f8c1c 100644 --- a/entry.go +++ b/entry.go @@ -51,13 +51,18 @@ func (entry *Entry) Reader() (*bytes.Buffer, error) { } serialized = append(serialized, '\n') } else { - levelText := strings.ToUpper(entry.Data["level"].(string)) + levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] levelColor := 34 - if levelText != "INFO" { + + 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 \x1b[%dm(\x1b[0m", levelColor, levelText, miniTS(), entry.Data["msg"], levelColor))...) + 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 @@ -77,10 +82,10 @@ func (entry *Entry) Reader() (*bytes.Buffer, error) { } else { serialized = append(serialized, ' ') } - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[34m%s\x1b[0m=%v", k, v))...) + 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, []byte(fmt.Sprintf("\x1b[%dm)\x1b[0m", levelColor))...) serialized = append(serialized, '\n') } @@ -113,15 +118,17 @@ func (entry *Entry) log(level string, msg string) string { fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) } - // 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()) + 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() From 52eb6b2fe675fd6d3462833579b01ca4ca0c0bc2 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Thu, 6 Mar 2014 21:20:13 -0500 Subject: [PATCH 13/18] readme: update --- README.md | 171 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 1449967..be34190 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,132 @@ # Logrus -Logrus is a simple, opinionated logging package for Go. Features include: +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. -* **Level logging**. Logrus has the levels: Debug, Info, Warning and Fatal. -* **Exceptions**. Warnings will log as an exception along with logging it to - out, without quitting. Fatal will do the same, but call `os.Exit(1)` after - emitting the exception. -* **JSON**. Logrus currently logs as JSON by default. +#### Fields -The API is completely compatible with the Go standard lib logger, with only the -features above added. - -## Motivation - -The motivation for this library came out of a pattern seen in Go applications me -and others have been writing with functions such as: +Logrus encourages careful, informative logging. It encourages the use of logging +fields, instead of long, unparseable error messages. For example, instead of: +`log.Fatalf("Failed to send event %s to topic %s with key %d")`, you should log +the much more discoverable: ```go -func reportFatalError(err error) { - airbrake.Notify(err) - log.Fatal(err) -} +log = logrus.New() +log.WithFields(&logrus.Fields{ + "event": event, + "topic": topic, + "key": key +}).Fatal("Failed to send event") +``` -func reportWarning(err error) { - airbrake.Notify(err) +We've found this API forces you to think about logging in a way that produces +much more useful logging messages. The `WithFields` call is optional. + +#### Hooks + +You can add hooks for logging levels. For example to send errors, to an +exception tracking service: + +```go +log.AddHook("error", func(entry logrus.Entry) { + err := airbrake.Notify(errors.New(entry.String())) + if err != nil { + log.WithFields(logrus.Fields{ + "source": "airbrake", + "endpoint": airbrake.Endpoint, + }).Info("Failed to send error to Airbrake") + } +}) +``` + +#### Errors + +You can also use Logrus to return errors with fields. For instance: + +```go +err := record.Destroy() +if err != nil { + return log.WithFields(&logrus.Fields{ + "id": record.Id, + "method": "destroy" + }).AsError("Failed to destroy record") } ``` -JSON logging is excellent for parsing logs for analysis and troubleshooting. -It's supported natively by log aggregators such as logstash and Splunk. Logging -JSON with logrus with the `WithFields` and `WithField` API in logrus forces you -to think about what context to log, to provide valuable troubleshoot information -later. +Will return a `logrus.Error` object. Passing it to +`log.{Info,Warn,Error,Fatal,Panic}` will log it according to the formatter set +for the environment. -## Example +#### Level logging + +Logrus has six levels: Debug, Info, Warning, Error, Fatal and Panic. ```go -import ( - "github.com/Sirupsen/logrus" -) +log.Info("Something noteworthy happened!") +log.Warn("You should probably take a look at this.") +log.Error("Something failed but I'm not quitting.") +log.Fatal("Bye.") +log.Panic("I'm bailing.") +``` -var logger logrus.New() -func main() { - 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" - // } +#### Entries - 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" - // } +Besides the fields added with `WithField` or `WithFields` some fields are +automatically added to all logging events: + +1. `time`. The timestamp when the entry was created. +2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after + the `AddFields` call. E.g. `Failed to send event.` +3. `level`. The logging level. E.g. `info`. +4. `file`. The file (and line) where the logging entry was created. E.g., + `main.go:82`. + +#### Environments + +Logrus has no notion of environment. If you wish for hooks and formatters to +only be used in specific environments, you should handle that yourself. For +example, if your application has a global variable `Environment`, which is a +string representation of the environment you could do: + +```go +init() { + // do something here to set environment depending on an environment variable + // or command-line flag + + if Environment == "production" { + log.SetFormatter(logrus.JSONFormatter) + } else { + // The TextFormatter is default, you don't actually have to do this. + log.SetFormatter(logrus.TextFormatter) + } } ``` -Using `Warning` and `Fatal` to log to `airbrake` requires setting -`airbrake.Endpoint` and `airbrake.ApiKey`. See -[tobi/airbrake-go](https://github.com/tobi/airbrake-go). +#### Formats + +The built in logging formatters are: + +* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise + without colors. Default for the development environment. +* `logrus.JSONFormatter`. Default for the production environment. + +You can define your formatter taking an entry. `entry.Data` is a `Fields` type +which is a `map[string]interface{}` with all your fields as well as the default +ones (see Entries above): + +```go +log.SetFormatter(func(entry *logrus.Entry) { + serialized, err = json.Marshal(entry.Data) + if err != nil { + return nil, log.WithFields(&logrus.Fields{ + "source": "log formatter", + "entry": entry.Data + }).AsError("Failed to serialize log entry to JSON") + } +}) +``` From 8492d88e94f9a11fe016b46ec0d183011830c33f Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Thu, 6 Mar 2014 21:26:05 -0500 Subject: [PATCH 14/18] readme: dont use printf --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be34190..416e1f6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ tracker. #### Fields Logrus encourages careful, informative logging. It encourages the use of logging -fields, instead of long, unparseable error messages. For example, instead of: +fields instead of long, unparseable error messages. For example, instead of: `log.Fatalf("Failed to send event %s to topic %s with key %d")`, you should log the much more discoverable: @@ -27,10 +27,14 @@ log.WithFields(&logrus.Fields{ We've found this API forces you to think about logging in a way that produces much more useful logging messages. The `WithFields` call is optional. +In general, with Logrus using any of the `printf`-family functions should be +seen as a hint you want to add a field, however, you can still use the +`printf`-family functions with Logrus. + #### Hooks -You can add hooks for logging levels. For example to send errors, to an -exception tracking service: +You can add hooks for logging levels. For example to send errors to an exception +tracking service: ```go log.AddHook("error", func(entry logrus.Entry) { From 7769e38becba167fe004f046edf19b4937355ddd Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Thu, 6 Mar 2014 21:49:10 -0500 Subject: [PATCH 15/18] readme: add setting level instructions --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 416e1f6..ddd8642 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ for the environment. Logrus has six levels: Debug, Info, Warning, Error, Fatal and Panic. ```go +log.Debug("Useful debugging information.") log.Info("Something noteworthy happened!") log.Warn("You should probably take a look at this.") log.Error("Something failed but I'm not quitting.") @@ -78,6 +79,13 @@ log.Fatal("Bye.") log.Panic("I'm bailing.") ``` +You can set the logging level: + +```go +// Will log anything that is info or above, default. +logrus.Level = LevelInfo +``` + #### Entries Besides the fields added with `WithField` or `WithFields` some fields are From 8fe53025b689960bcd4cda4a896460c4dd9c20aa Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Fri, 7 Mar 2014 12:51:29 -0500 Subject: [PATCH 16/18] readme: update after removing env --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddd8642..232f74e 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,8 @@ init() { The built in logging formatters are: * `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise - without colors. Default for the development environment. -* `logrus.JSONFormatter`. Default for the production environment. + without colors. +* `logrus.JSONFormatter`. Logs fields as JSON. You can define your formatter taking an entry. `entry.Data` is a `Fields` type which is a `map[string]interface{}` with all your fields as well as the default From e155f76d1bb46189c582ab9eac6943b1cb8c023c Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Sat, 8 Mar 2014 10:55:15 -0500 Subject: [PATCH 17/18] readme: remove error --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index 232f74e..e56ca77 100644 --- a/README.md +++ b/README.md @@ -48,24 +48,6 @@ log.AddHook("error", func(entry logrus.Entry) { }) ``` -#### Errors - -You can also use Logrus to return errors with fields. For instance: - -```go -err := record.Destroy() -if err != nil { - return log.WithFields(&logrus.Fields{ - "id": record.Id, - "method": "destroy" - }).AsError("Failed to destroy record") -} -``` - -Will return a `logrus.Error` object. Passing it to -`log.{Info,Warn,Error,Fatal,Panic}` will log it according to the formatter set -for the environment. - #### Level logging Logrus has six levels: Debug, Info, Warning, Error, Fatal and Panic. From 53371e36641339bc393fe4e2d1fcf25d84bd0208 Mon Sep 17 00:00:00 2001 From: Simon Eskildsen Date: Mon, 10 Mar 2014 19:22:08 -0400 Subject: [PATCH 18/18] 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 +}