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

141
entry.go
View File

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

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

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

View File

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

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
}