diff --git a/README.md b/README.md index cad2676..7d677a5 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,121 @@ # Logrus -Logrus is a simple, opinionated logging package for Go. It has three debugging -levels: +Logrus is a simple, opinionated structured logging package for Go which is +completely API compatible with the standard library logger. -* `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`. +#### Fields -## Usage - -The global logging level is set by: `logrus.Level = logrus.{LevelDebug,LevelWarning,LevelFatal}`. - -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: +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 -logrus.Debug("Something debugworthy happened: %s", importantStuff) -logrus.Info("Something infoworthy happened: %s", importantStuff) - -logrus.Warning("Something bad happened: %s", importantStuff) -// Reports to Airbrake - -logrus.Fatal("Something fatal happened: %s", importantStuff) -// Reports to Airbrake -// Then exits +log = logrus.New() +log.WithFields(&logrus.Fields{ + "event": event, + "topic": topic, + "key": key +}).Fatal("Failed to send event") ``` -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. +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: ```go -type Walrus struct { - TuskSize uint64 - Sex bool - logger logrus.Logger -} - -func NewWalrus(tuskSize uint64, sex bool) *Walrus { - return &Walrus{ - TuskSize: tuskSize, - Sex: bool, - logger: logrus.NewLogger("Walrus"), +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") } -} +}) +``` -func (walrus *Walrus) Mate(partner *Walrus) error { - if walrus.Sex == partner.Sex { - return errors.New("Incompatible mating partner.") +#### Level logging + +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.") +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 +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) } - - 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 } ``` + +#### Formats + +The built in logging formatters are: + +* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise + 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 +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") + } +}) +``` diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..d79e345 --- /dev/null +++ b/entry.go @@ -0,0 +1,199 @@ +package logrus + +import ( + "bytes" + "fmt" + "io" + "os" + "time" +) + +type Entry struct { + logger *Logger + 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, + // Default is three fields, give a little extra room + Data: make(Fields, 5), + } +} + +func (entry *Entry) Reader() (*bytes.Buffer, error) { + serialized, err := entry.logger.Formatter.Format(entry) + return bytes.NewBuffer(serialized), err +} + +func (entry *Entry) String() (string, error) { + reader, err := entry.Reader() + if err != nil { + return "", err + } + + return reader.String(), err +} + +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) string { + entry.Data["time"] = time.Now().String() + entry.Data["level"] = level + entry.Data["msg"] = msg + + reader, err := entry.Reader() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) + } + + 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 write to log, %v", err) + } + + return reader.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) 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) +} + +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...)) +} + +// 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) Warnf(format string, args ...interface{}) { + entry.Warn(fmt.Sprintf(format, args...)) +} + +func (entry *Entry) Warningf(format string, args ...interface{}) { + 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{}) { + 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.Sprint(args...)) +} + +func (entry *Entry) Infoln(args ...interface{}) { + entry.Info(fmt.Sprint(args...)) +} + +func (entry *Entry) Println(args ...interface{}) { + entry.Print(fmt.Sprint(args...)) +} + +func (entry *Entry) Warnln(args ...interface{}) { + entry.Warn(fmt.Sprint(args...)) +} + +func (entry *Entry) Warningln(args ...interface{}) { + 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.Sprint(args...)) +} + +func (entry *Entry) Panicln(args ...interface{}) { + 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/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 new file mode 100644 index 0000000..3379469 --- /dev/null +++ b/logger.go @@ -0,0 +1,132 @@ +package logrus + +import ( + "io" + "os" + "sync" +) + +type Logger struct { + 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. + Formatter: new(TextFormatter), + Hooks: make(levelHooks), + } +} + +func (logger *Logger) WithField(key string, value interface{}) *Entry { + return NewEntry(logger).WithField(key, value) +} + +func (logger *Logger) WithFields(fields Fields) *Entry { + return NewEntry(logger).WithFields(fields) +} + +// 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) Warnf(format string, args ...interface{}) { + NewEntry(logger).Warnf(format, args...) +} + +func (logger *Logger) Warningf(format string, args ...interface{}) { + 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{}) { + 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) Warn(args ...interface{}) { + NewEntry(logger).Warn(args...) +} + +func (logger *Logger) Warning(args ...interface{}) { + NewEntry(logger).Warn(args...) +} + +func (logger *Logger) Error(args ...interface{}) { + NewEntry(logger).Error(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) Warnln(args ...interface{}) { + NewEntry(logger).Warnln(args...) +} + +func (logger *Logger) Warningln(args ...interface{}) { + NewEntry(logger).Warnln(args...) +} + +func (logger *Logger) Errorln(args ...interface{}) { + NewEntry(logger).Errorln(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 new file mode 100644 index 0000000..bff336f --- /dev/null +++ b/logrus.go @@ -0,0 +1,33 @@ +package logrus + +type Fields map[string]interface{} + +type LevelType uint8 + +const ( + LevelPanic LevelType = iota + LevelFatal + LevelError + LevelWarn + LevelInfo + LevelDebug +) + +var Level LevelType = LevelInfo + +// 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{}) +} 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 +}