Add features from README

This commit is contained in:
Simon Eskildsen 2014-03-10 19:22:08 -04:00
parent e155f76d1b
commit 53371e3664
11 changed files with 285 additions and 178 deletions

View File

@ -1,12 +1,7 @@
# Logrus # Logrus
Logrus is a simple, opinionated logging package for Go which is completely API Logrus is a simple, opinionated structured logging package for Go which is
compatible with the standard library logger. It has six logging levels: Debug, completely API compatible with the standard library logger.
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.
#### Fields #### Fields

139
entry.go
View File

@ -2,17 +2,10 @@ package logrus
import ( import (
"bytes" "bytes"
"encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"sort"
"strings"
"time" "time"
"github.com/burke/ttyutils"
"github.com/tobi/airbrake-go"
) )
type Entry struct { type Entry struct {
@ -33,64 +26,23 @@ func miniTS() int {
func NewEntry(logger *Logger) *Entry { func NewEntry(logger *Logger) *Entry {
return &Entry{ return &Entry{
logger: logger, logger: logger,
// Default is three fields, give a little extra room. Shouldn't hurt the // Default is three fields, give a little extra room
// scale.
Data: make(Fields, 5), Data: make(Fields, 5),
} }
} }
// TODO: Other formats?
func (entry *Entry) Reader() (*bytes.Buffer, error) { func (entry *Entry) Reader() (*bytes.Buffer, error) {
var serialized []byte serialized, err := entry.logger.Formatter.Format(entry)
var err error return bytes.NewBuffer(serialized), err
}
if Environment == "production" { func (entry *Entry) String() (string, error) {
serialized, err = json.Marshal(entry.Data) reader, err := entry.Reader()
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) return "", 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()) { return reader.String(), err
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')
}
return bytes.NewBuffer(serialized), nil
} }
func (entry *Entry) WithField(key string, value interface{}) *Entry { 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 { for key, value := range fields {
entry.WithField(key, value) entry.WithField(key, value)
} }
return entry return entry
} }
func (entry *Entry) log(level string, msg string) 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["time"] = time.Now().String()
entry.Data["level"] = level entry.Data["level"] = level
// TODO: Is this the best name?
entry.Data["msg"] = msg entry.Data["msg"] = msg
reader, err := entry.Reader() 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) 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() entry.logger.mu.Lock()
defer entry.logger.mu.Unlock() defer entry.logger.mu.Unlock()
_, err = io.Copy(entry.logger.Out, reader) _, err = io.Copy(entry.logger.Out, reader)
if err != nil { 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() return reader.String()
@ -145,30 +81,42 @@ func (entry *Entry) log(level string, msg string) string {
func (entry *Entry) Debug(args ...interface{}) { func (entry *Entry) Debug(args ...interface{}) {
if Level >= LevelDebug { if Level >= LevelDebug {
entry.log("debug", fmt.Sprint(args...)) entry.log("debug", fmt.Sprint(args...))
entry.logger.Hooks.Fire(LevelDebug, entry)
} }
} }
func (entry *Entry) Info(args ...interface{}) { func (entry *Entry) Info(args ...interface{}) {
if Level >= LevelInfo { if Level >= LevelInfo {
entry.log("info", fmt.Sprint(args...)) entry.log("info", fmt.Sprint(args...))
entry.logger.Hooks.Fire(LevelInfo, entry)
} }
} }
func (entry *Entry) Print(args ...interface{}) { func (entry *Entry) Print(args ...interface{}) {
if Level >= LevelInfo { if Level >= LevelInfo {
entry.log("info", fmt.Sprint(args...)) entry.log("info", fmt.Sprint(args...))
entry.logger.Hooks.Fire(LevelInfo, entry)
} }
} }
func (entry *Entry) Warning(args ...interface{}) { func (entry *Entry) Warn(args ...interface{}) {
if Level >= LevelWarning { if Level >= LevelWarn {
entry.log("warning", fmt.Sprint(args...)) 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{}) { func (entry *Entry) Fatal(args ...interface{}) {
if Level >= LevelFatal { if Level >= LevelFatal {
entry.log("fatal", fmt.Sprint(args...)) entry.log("fatal", fmt.Sprint(args...))
entry.logger.Hooks.Fire(LevelFatal, entry)
} }
os.Exit(1) os.Exit(1)
} }
@ -176,6 +124,7 @@ func (entry *Entry) Fatal(args ...interface{}) {
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
if Level >= LevelPanic { if Level >= LevelPanic {
msg := entry.log("panic", fmt.Sprint(args...)) msg := entry.log("panic", fmt.Sprint(args...))
entry.logger.Hooks.Fire(LevelPanic, entry)
panic(msg) panic(msg)
} }
panic(fmt.Sprint(args...)) panic(fmt.Sprint(args...))
@ -195,8 +144,16 @@ func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Print(fmt.Sprintf(format, args...)) 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{}) { 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{}) { func (entry *Entry) Fatalf(format string, args ...interface{}) {
@ -210,35 +167,33 @@ func (entry *Entry) Panicf(format string, args ...interface{}) {
// Entry Println family functions // Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) { func (entry *Entry) Debugln(args ...interface{}) {
entry.Debug(fmt.Sprintln(args...)) entry.Debug(fmt.Sprint(args...))
} }
func (entry *Entry) Infoln(args ...interface{}) { func (entry *Entry) Infoln(args ...interface{}) {
entry.Info(fmt.Sprintln(args...)) entry.Info(fmt.Sprint(args...))
} }
func (entry *Entry) Println(args ...interface{}) { 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{}) { 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{}) { func (entry *Entry) Fatalln(args ...interface{}) {
entry.Fatal(fmt.Sprintln(args...)) entry.Fatal(fmt.Sprint(args...))
} }
func (entry *Entry) Panicln(args ...interface{}) { func (entry *Entry) Panicln(args ...interface{}) {
entry.Panic(fmt.Sprintln(args...)) entry.Panic(fmt.Sprint(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")
}
} }

36
examples/text.go Normal file
View File

@ -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")
}
}

View File

@ -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"
// }
}

5
formatter.go Normal file
View File

@ -0,0 +1,5 @@
package logrus
type Formatter interface {
Format(*Entry) ([]byte, error)
}

28
hooks.go Normal file
View File

@ -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
}

17
json_formatter.go Normal file
View File

@ -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
}

View File

@ -8,12 +8,16 @@ import (
type Logger struct { type Logger struct {
Out io.Writer Out io.Writer
Hooks levelHooks
Formatter Formatter
mu sync.Mutex mu sync.Mutex
} }
func New() *Logger { func New() *Logger {
return &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...) 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{}) { 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{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
@ -65,8 +77,16 @@ func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Print(args...) NewEntry(logger).Print(args...)
} }
func (logger *Logger) Warn(args ...interface{}) {
NewEntry(logger).Warn(args...)
}
func (logger *Logger) Warning(args ...interface{}) { 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{}) { func (logger *Logger) Fatal(args ...interface{}) {
@ -91,8 +111,16 @@ func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...) NewEntry(logger).Println(args...)
} }
func (logger *Logger) Warnln(args ...interface{}) {
NewEntry(logger).Warnln(args...)
}
func (logger *Logger) Warningln(args ...interface{}) { 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{}) { func (logger *Logger) Fatalln(args ...interface{}) {

View File

@ -1,24 +1,19 @@
package logrus 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 Fields map[string]interface{}
type LevelType uint8
const ( const (
LevelPanic LevelType = iota LevelPanic LevelType = iota
LevelFatal LevelFatal
LevelWarning LevelError
LevelWarn
LevelInfo LevelInfo
LevelDebug LevelDebug
) )
var Level LevelType = LevelInfo var Level LevelType = LevelInfo
var Environment string = "development"
// StandardLogger is what your logrus-enabled library should take, that way // 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 // it'll accept a stdlib logger and a logrus logger. There's no standard

52
logrus_test.go Normal file
View File

@ -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")
})
}

59
text_formatter.go Normal file
View File

@ -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
}