parent
28641dc525
commit
93b406c986
@ -0,0 +1,47 @@ |
|||||||
|
# 0.8.7 |
||||||
|
|
||||||
|
* logrus/core: fix possible race (#216) |
||||||
|
* logrus/doc: small typo fixes and doc improvements |
||||||
|
|
||||||
|
|
||||||
|
# 0.8.6 |
||||||
|
|
||||||
|
* hooks/raven: allow passing an initialized client |
||||||
|
|
||||||
|
# 0.8.5 |
||||||
|
|
||||||
|
* logrus/core: revert #208 |
||||||
|
|
||||||
|
# 0.8.4 |
||||||
|
|
||||||
|
* formatter/text: fix data race (#218) |
||||||
|
|
||||||
|
# 0.8.3 |
||||||
|
|
||||||
|
* logrus/core: fix entry log level (#208) |
||||||
|
* logrus/core: improve performance of text formatter by 40% |
||||||
|
* logrus/core: expose `LevelHooks` type |
||||||
|
* logrus/core: add support for DragonflyBSD and NetBSD |
||||||
|
* formatter/text: print structs more verbosely |
||||||
|
|
||||||
|
# 0.8.2 |
||||||
|
|
||||||
|
* logrus: fix more Fatal family functions |
||||||
|
|
||||||
|
# 0.8.1 |
||||||
|
|
||||||
|
* logrus: fix not exiting on `Fatalf` and `Fatalln` |
||||||
|
|
||||||
|
# 0.8.0 |
||||||
|
|
||||||
|
* logrus: defaults to stderr instead of stdout |
||||||
|
* hooks/sentry: add special field for `*http.Request` |
||||||
|
* formatter/text: ignore Windows for colors |
||||||
|
|
||||||
|
# 0.7.3 |
||||||
|
|
||||||
|
* formatter/\*: allow configuration of timestamp layout |
||||||
|
|
||||||
|
# 0.7.2 |
||||||
|
|
||||||
|
* formatter/text: Add configuration option for time format (#158) |
@ -0,0 +1,21 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014 Simon Eskildsen |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in |
||||||
|
all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
@ -0,0 +1,26 @@ |
|||||||
|
/* |
||||||
|
Package logrus is a structured logger for Go, completely API compatible with the standard library logger. |
||||||
|
|
||||||
|
|
||||||
|
The simplest way to use Logrus is simply the package-level exported logger: |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
log "github.com/Sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
log.WithFields(log.Fields{ |
||||||
|
"animal": "walrus", |
||||||
|
"number": 1, |
||||||
|
"size": 10, |
||||||
|
}).Info("A walrus appears") |
||||||
|
} |
||||||
|
|
||||||
|
Output: |
||||||
|
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 |
||||||
|
|
||||||
|
For a full guide visit https://github.com/Sirupsen/logrus
|
||||||
|
*/ |
||||||
|
package logrus |
@ -0,0 +1,264 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Defines the key when adding errors using WithError.
|
||||||
|
var ErrorKey = "error" |
||||||
|
|
||||||
|
// An entry is the final or intermediate Logrus logging entry. It contains all
|
||||||
|
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
|
||||||
|
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
|
||||||
|
// passed around as much as you wish to avoid field duplication.
|
||||||
|
type Entry struct { |
||||||
|
Logger *Logger |
||||||
|
|
||||||
|
// Contains all the fields set by the user.
|
||||||
|
Data Fields |
||||||
|
|
||||||
|
// Time at which the log entry was created
|
||||||
|
Time time.Time |
||||||
|
|
||||||
|
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
|
||||||
|
Level Level |
||||||
|
|
||||||
|
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
|
||||||
|
Message string |
||||||
|
} |
||||||
|
|
||||||
|
func NewEntry(logger *Logger) *Entry { |
||||||
|
return &Entry{ |
||||||
|
Logger: logger, |
||||||
|
// Default is three fields, give a little extra room
|
||||||
|
Data: make(Fields, 5), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a reader for the entry, which is a proxy to the formatter.
|
||||||
|
func (entry *Entry) Reader() (*bytes.Buffer, error) { |
||||||
|
serialized, err := entry.Logger.Formatter.Format(entry) |
||||||
|
return bytes.NewBuffer(serialized), err |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the string representation from the reader and ultimately the
|
||||||
|
// formatter.
|
||||||
|
func (entry *Entry) String() (string, error) { |
||||||
|
reader, err := entry.Reader() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return reader.String(), err |
||||||
|
} |
||||||
|
|
||||||
|
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
|
||||||
|
func (entry *Entry) WithError(err error) *Entry { |
||||||
|
return entry.WithField(ErrorKey, err) |
||||||
|
} |
||||||
|
|
||||||
|
// Add a single field to the Entry.
|
||||||
|
func (entry *Entry) WithField(key string, value interface{}) *Entry { |
||||||
|
return entry.WithFields(Fields{key: value}) |
||||||
|
} |
||||||
|
|
||||||
|
// Add a map of fields to the Entry.
|
||||||
|
func (entry *Entry) WithFields(fields Fields) *Entry { |
||||||
|
data := Fields{} |
||||||
|
for k, v := range entry.Data { |
||||||
|
data[k] = v |
||||||
|
} |
||||||
|
for k, v := range fields { |
||||||
|
data[k] = v |
||||||
|
} |
||||||
|
return &Entry{Logger: entry.Logger, Data: data} |
||||||
|
} |
||||||
|
|
||||||
|
// This function is not declared with a pointer value because otherwise
|
||||||
|
// race conditions will occur when using multiple goroutines
|
||||||
|
func (entry Entry) log(level Level, msg string) { |
||||||
|
entry.Time = time.Now() |
||||||
|
entry.Level = level |
||||||
|
entry.Message = msg |
||||||
|
|
||||||
|
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil { |
||||||
|
entry.Logger.mu.Lock() |
||||||
|
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) |
||||||
|
entry.Logger.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
reader, err := entry.Reader() |
||||||
|
if err != nil { |
||||||
|
entry.Logger.mu.Lock() |
||||||
|
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) |
||||||
|
entry.Logger.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
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\n", err) |
||||||
|
} |
||||||
|
|
||||||
|
// To avoid Entry#log() returning a value that only would make sense for
|
||||||
|
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
||||||
|
// directly here.
|
||||||
|
if level <= PanicLevel { |
||||||
|
panic(&entry) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Debug(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= DebugLevel { |
||||||
|
entry.log(DebugLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Print(args ...interface{}) { |
||||||
|
entry.Info(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Info(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= InfoLevel { |
||||||
|
entry.log(InfoLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warn(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= WarnLevel { |
||||||
|
entry.log(WarnLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warning(args ...interface{}) { |
||||||
|
entry.Warn(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Error(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= ErrorLevel { |
||||||
|
entry.log(ErrorLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Fatal(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= FatalLevel { |
||||||
|
entry.log(FatalLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Panic(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= PanicLevel { |
||||||
|
entry.log(PanicLevel, fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
panic(fmt.Sprint(args...)) |
||||||
|
} |
||||||
|
|
||||||
|
// Entry Printf family functions
|
||||||
|
|
||||||
|
func (entry *Entry) Debugf(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= DebugLevel { |
||||||
|
entry.Debug(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Infof(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= InfoLevel { |
||||||
|
entry.Info(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Printf(format string, args ...interface{}) { |
||||||
|
entry.Infof(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warnf(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= WarnLevel { |
||||||
|
entry.Warn(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warningf(format string, args ...interface{}) { |
||||||
|
entry.Warnf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Errorf(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= ErrorLevel { |
||||||
|
entry.Error(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Fatalf(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= FatalLevel { |
||||||
|
entry.Fatal(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Panicf(format string, args ...interface{}) { |
||||||
|
if entry.Logger.Level >= PanicLevel { |
||||||
|
entry.Panic(fmt.Sprintf(format, args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Entry Println family functions
|
||||||
|
|
||||||
|
func (entry *Entry) Debugln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= DebugLevel { |
||||||
|
entry.Debug(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Infoln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= InfoLevel { |
||||||
|
entry.Info(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Println(args ...interface{}) { |
||||||
|
entry.Infoln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warnln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= WarnLevel { |
||||||
|
entry.Warn(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Warningln(args ...interface{}) { |
||||||
|
entry.Warnln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Errorln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= ErrorLevel { |
||||||
|
entry.Error(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Fatalln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= FatalLevel { |
||||||
|
entry.Fatal(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (entry *Entry) Panicln(args ...interface{}) { |
||||||
|
if entry.Logger.Level >= PanicLevel { |
||||||
|
entry.Panic(entry.sprintlnn(args...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Sprintlnn => Sprint no newline. This is to get the behavior of how
|
||||||
|
// fmt.Sprintln where spaces are always added between operands, regardless of
|
||||||
|
// their type. Instead of vendoring the Sprintln implementation to spare a
|
||||||
|
// string allocation, we do the simplest thing.
|
||||||
|
func (entry *Entry) sprintlnn(args ...interface{}) string { |
||||||
|
msg := fmt.Sprintln(args...) |
||||||
|
return msg[:len(msg)-1] |
||||||
|
} |
@ -0,0 +1,77 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestEntryWithError(t *testing.T) { |
||||||
|
|
||||||
|
assert := assert.New(t) |
||||||
|
|
||||||
|
defer func() { |
||||||
|
ErrorKey = "error" |
||||||
|
}() |
||||||
|
|
||||||
|
err := fmt.Errorf("kaboom at layer %d", 4711) |
||||||
|
|
||||||
|
assert.Equal(err, WithError(err).Data["error"]) |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &bytes.Buffer{} |
||||||
|
entry := NewEntry(logger) |
||||||
|
|
||||||
|
assert.Equal(err, entry.WithError(err).Data["error"]) |
||||||
|
|
||||||
|
ErrorKey = "err" |
||||||
|
|
||||||
|
assert.Equal(err, entry.WithError(err).Data["err"]) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func TestEntryPanicln(t *testing.T) { |
||||||
|
errBoom := fmt.Errorf("boom time") |
||||||
|
|
||||||
|
defer func() { |
||||||
|
p := recover() |
||||||
|
assert.NotNil(t, p) |
||||||
|
|
||||||
|
switch pVal := p.(type) { |
||||||
|
case *Entry: |
||||||
|
assert.Equal(t, "kaboom", pVal.Message) |
||||||
|
assert.Equal(t, errBoom, pVal.Data["err"]) |
||||||
|
default: |
||||||
|
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &bytes.Buffer{} |
||||||
|
entry := NewEntry(logger) |
||||||
|
entry.WithField("err", errBoom).Panicln("kaboom") |
||||||
|
} |
||||||
|
|
||||||
|
func TestEntryPanicf(t *testing.T) { |
||||||
|
errBoom := fmt.Errorf("boom again") |
||||||
|
|
||||||
|
defer func() { |
||||||
|
p := recover() |
||||||
|
assert.NotNil(t, p) |
||||||
|
|
||||||
|
switch pVal := p.(type) { |
||||||
|
case *Entry: |
||||||
|
assert.Equal(t, "kaboom true", pVal.Message) |
||||||
|
assert.Equal(t, errBoom, pVal.Data["err"]) |
||||||
|
default: |
||||||
|
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &bytes.Buffer{} |
||||||
|
entry := NewEntry(logger) |
||||||
|
entry.WithField("err", errBoom).Panicf("kaboom %v", true) |
||||||
|
} |
@ -0,0 +1,193 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// std is the name of the standard logger in stdlib `log`
|
||||||
|
std = New() |
||||||
|
) |
||||||
|
|
||||||
|
func StandardLogger() *Logger { |
||||||
|
return std |
||||||
|
} |
||||||
|
|
||||||
|
// SetOutput sets the standard logger output.
|
||||||
|
func SetOutput(out io.Writer) { |
||||||
|
std.mu.Lock() |
||||||
|
defer std.mu.Unlock() |
||||||
|
std.Out = out |
||||||
|
} |
||||||
|
|
||||||
|
// SetFormatter sets the standard logger formatter.
|
||||||
|
func SetFormatter(formatter Formatter) { |
||||||
|
std.mu.Lock() |
||||||
|
defer std.mu.Unlock() |
||||||
|
std.Formatter = formatter |
||||||
|
} |
||||||
|
|
||||||
|
// SetLevel sets the standard logger level.
|
||||||
|
func SetLevel(level Level) { |
||||||
|
std.mu.Lock() |
||||||
|
defer std.mu.Unlock() |
||||||
|
std.Level = level |
||||||
|
} |
||||||
|
|
||||||
|
// GetLevel returns the standard logger level.
|
||||||
|
func GetLevel() Level { |
||||||
|
std.mu.Lock() |
||||||
|
defer std.mu.Unlock() |
||||||
|
return std.Level |
||||||
|
} |
||||||
|
|
||||||
|
// AddHook adds a hook to the standard logger hooks.
|
||||||
|
func AddHook(hook Hook) { |
||||||
|
std.mu.Lock() |
||||||
|
defer std.mu.Unlock() |
||||||
|
std.Hooks.Add(hook) |
||||||
|
} |
||||||
|
|
||||||
|
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
|
||||||
|
func WithError(err error) *Entry { |
||||||
|
return std.WithField(ErrorKey, err) |
||||||
|
} |
||||||
|
|
||||||
|
// WithField creates an entry from the standard logger and adds a field to
|
||||||
|
// it. If you want multiple fields, use `WithFields`.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithField(key string, value interface{}) *Entry { |
||||||
|
return std.WithField(key, value) |
||||||
|
} |
||||||
|
|
||||||
|
// WithFields creates an entry from the standard logger and adds multiple
|
||||||
|
// fields to it. This is simply a helper for `WithField`, invoking it
|
||||||
|
// once for each field.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithFields(fields Fields) *Entry { |
||||||
|
return std.WithFields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
// Debug logs a message at level Debug on the standard logger.
|
||||||
|
func Debug(args ...interface{}) { |
||||||
|
std.Debug(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Print logs a message at level Info on the standard logger.
|
||||||
|
func Print(args ...interface{}) { |
||||||
|
std.Print(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Info logs a message at level Info on the standard logger.
|
||||||
|
func Info(args ...interface{}) { |
||||||
|
std.Info(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warn logs a message at level Warn on the standard logger.
|
||||||
|
func Warn(args ...interface{}) { |
||||||
|
std.Warn(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warning logs a message at level Warn on the standard logger.
|
||||||
|
func Warning(args ...interface{}) { |
||||||
|
std.Warning(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Error logs a message at level Error on the standard logger.
|
||||||
|
func Error(args ...interface{}) { |
||||||
|
std.Error(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Panic logs a message at level Panic on the standard logger.
|
||||||
|
func Panic(args ...interface{}) { |
||||||
|
std.Panic(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatal logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatal(args ...interface{}) { |
||||||
|
std.Fatal(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Debugf logs a message at level Debug on the standard logger.
|
||||||
|
func Debugf(format string, args ...interface{}) { |
||||||
|
std.Debugf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Printf logs a message at level Info on the standard logger.
|
||||||
|
func Printf(format string, args ...interface{}) { |
||||||
|
std.Printf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Infof logs a message at level Info on the standard logger.
|
||||||
|
func Infof(format string, args ...interface{}) { |
||||||
|
std.Infof(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warnf logs a message at level Warn on the standard logger.
|
||||||
|
func Warnf(format string, args ...interface{}) { |
||||||
|
std.Warnf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warningf logs a message at level Warn on the standard logger.
|
||||||
|
func Warningf(format string, args ...interface{}) { |
||||||
|
std.Warningf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Errorf logs a message at level Error on the standard logger.
|
||||||
|
func Errorf(format string, args ...interface{}) { |
||||||
|
std.Errorf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Panicf logs a message at level Panic on the standard logger.
|
||||||
|
func Panicf(format string, args ...interface{}) { |
||||||
|
std.Panicf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatalf logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatalf(format string, args ...interface{}) { |
||||||
|
std.Fatalf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Debugln logs a message at level Debug on the standard logger.
|
||||||
|
func Debugln(args ...interface{}) { |
||||||
|
std.Debugln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Println logs a message at level Info on the standard logger.
|
||||||
|
func Println(args ...interface{}) { |
||||||
|
std.Println(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Infoln logs a message at level Info on the standard logger.
|
||||||
|
func Infoln(args ...interface{}) { |
||||||
|
std.Infoln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warnln logs a message at level Warn on the standard logger.
|
||||||
|
func Warnln(args ...interface{}) { |
||||||
|
std.Warnln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Warningln logs a message at level Warn on the standard logger.
|
||||||
|
func Warningln(args ...interface{}) { |
||||||
|
std.Warningln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Errorln logs a message at level Error on the standard logger.
|
||||||
|
func Errorln(args ...interface{}) { |
||||||
|
std.Errorln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Panicln logs a message at level Panic on the standard logger.
|
||||||
|
func Panicln(args ...interface{}) { |
||||||
|
std.Panicln(args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fatalln logs a message at level Fatal on the standard logger.
|
||||||
|
func Fatalln(args ...interface{}) { |
||||||
|
std.Fatalln(args...) |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
const DefaultTimestampFormat = time.RFC3339 |
||||||
|
|
||||||
|
// The Formatter interface is used to implement a custom Formatter. It takes an
|
||||||
|
// `Entry`. It exposes all the fields, including the default ones:
|
||||||
|
//
|
||||||
|
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
|
||||||
|
// * `entry.Data["time"]`. The timestamp.
|
||||||
|
// * `entry.Data["level"]. The level the entry was logged at.
|
||||||
|
//
|
||||||
|
// Any additional fields added with `WithField` or `WithFields` are also in
|
||||||
|
// `entry.Data`. Format is expected to return an array of bytes which are then
|
||||||
|
// logged to `logger.Out`.
|
||||||
|
type Formatter interface { |
||||||
|
Format(*Entry) ([]byte, error) |
||||||
|
} |
||||||
|
|
||||||
|
// This is to not silently overwrite `time`, `msg` and `level` fields when
|
||||||
|
// dumping it. If this code wasn't there doing:
|
||||||
|
//
|
||||||
|
// logrus.WithField("level", 1).Info("hello")
|
||||||
|
//
|
||||||
|
// Would just silently drop the user provided level. Instead with this code
|
||||||
|
// it'll logged as:
|
||||||
|
//
|
||||||
|
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
|
||||||
|
//
|
||||||
|
// It's not exported because it's still using Data in an opinionated way. It's to
|
||||||
|
// avoid code duplication between the two default formatters.
|
||||||
|
func prefixFieldClashes(data Fields) { |
||||||
|
_, ok := data["time"] |
||||||
|
if ok { |
||||||
|
data["fields.time"] = data["time"] |
||||||
|
} |
||||||
|
|
||||||
|
_, ok = data["msg"] |
||||||
|
if ok { |
||||||
|
data["fields.msg"] = data["msg"] |
||||||
|
} |
||||||
|
|
||||||
|
_, ok = data["level"] |
||||||
|
if ok { |
||||||
|
data["fields.level"] = data["level"] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// smallFields is a small size data set for benchmarking
|
||||||
|
var smallFields = Fields{ |
||||||
|
"foo": "bar", |
||||||
|
"baz": "qux", |
||||||
|
"one": "two", |
||||||
|
"three": "four", |
||||||
|
} |
||||||
|
|
||||||
|
// largeFields is a large size data set for benchmarking
|
||||||
|
var largeFields = Fields{ |
||||||
|
"foo": "bar", |
||||||
|
"baz": "qux", |
||||||
|
"one": "two", |
||||||
|
"three": "four", |
||||||
|
"five": "six", |
||||||
|
"seven": "eight", |
||||||
|
"nine": "ten", |
||||||
|
"eleven": "twelve", |
||||||
|
"thirteen": "fourteen", |
||||||
|
"fifteen": "sixteen", |
||||||
|
"seventeen": "eighteen", |
||||||
|
"nineteen": "twenty", |
||||||
|
"a": "b", |
||||||
|
"c": "d", |
||||||
|
"e": "f", |
||||||
|
"g": "h", |
||||||
|
"i": "j", |
||||||
|
"k": "l", |
||||||
|
"m": "n", |
||||||
|
"o": "p", |
||||||
|
"q": "r", |
||||||
|
"s": "t", |
||||||
|
"u": "v", |
||||||
|
"w": "x", |
||||||
|
"y": "z", |
||||||
|
"this": "will", |
||||||
|
"make": "thirty", |
||||||
|
"entries": "yeah", |
||||||
|
} |
||||||
|
|
||||||
|
var errorFields = Fields{ |
||||||
|
"foo": fmt.Errorf("bar"), |
||||||
|
"baz": fmt.Errorf("qux"), |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkErrorTextFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &TextFormatter{DisableColors: true}, errorFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkSmallTextFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkLargeTextFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &TextFormatter{DisableColors: true}, largeFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkSmallColoredTextFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &TextFormatter{ForceColors: true}, smallFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkLargeColoredTextFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &TextFormatter{ForceColors: true}, largeFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkSmallJSONFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &JSONFormatter{}, smallFields) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkLargeJSONFormatter(b *testing.B) { |
||||||
|
doBenchmark(b, &JSONFormatter{}, largeFields) |
||||||
|
} |
||||||
|
|
||||||
|
func doBenchmark(b *testing.B, formatter Formatter, fields Fields) { |
||||||
|
entry := &Entry{ |
||||||
|
Time: time.Time{}, |
||||||
|
Level: InfoLevel, |
||||||
|
Message: "message", |
||||||
|
Data: fields, |
||||||
|
} |
||||||
|
var d []byte |
||||||
|
var err error |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
d, err = formatter.Format(entry) |
||||||
|
if err != nil { |
||||||
|
b.Fatal(err) |
||||||
|
} |
||||||
|
b.SetBytes(int64(len(d))) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
type TestHook struct { |
||||||
|
Fired bool |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *TestHook) Fire(entry *Entry) error { |
||||||
|
hook.Fired = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *TestHook) Levels() []Level { |
||||||
|
return []Level{ |
||||||
|
DebugLevel, |
||||||
|
InfoLevel, |
||||||
|
WarnLevel, |
||||||
|
ErrorLevel, |
||||||
|
FatalLevel, |
||||||
|
PanicLevel, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestHookFires(t *testing.T) { |
||||||
|
hook := new(TestHook) |
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Hooks.Add(hook) |
||||||
|
assert.Equal(t, hook.Fired, false) |
||||||
|
|
||||||
|
log.Print("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, hook.Fired, true) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
type ModifyHook struct { |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *ModifyHook) Fire(entry *Entry) error { |
||||||
|
entry.Data["wow"] = "whale" |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *ModifyHook) Levels() []Level { |
||||||
|
return []Level{ |
||||||
|
DebugLevel, |
||||||
|
InfoLevel, |
||||||
|
WarnLevel, |
||||||
|
ErrorLevel, |
||||||
|
FatalLevel, |
||||||
|
PanicLevel, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestHookCanModifyEntry(t *testing.T) { |
||||||
|
hook := new(ModifyHook) |
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Hooks.Add(hook) |
||||||
|
log.WithField("wow", "elephant").Print("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["wow"], "whale") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCanFireMultipleHooks(t *testing.T) { |
||||||
|
hook1 := new(ModifyHook) |
||||||
|
hook2 := new(TestHook) |
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Hooks.Add(hook1) |
||||||
|
log.Hooks.Add(hook2) |
||||||
|
|
||||||
|
log.WithField("wow", "elephant").Print("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["wow"], "whale") |
||||||
|
assert.Equal(t, hook2.Fired, true) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
type ErrorHook struct { |
||||||
|
Fired bool |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *ErrorHook) Fire(entry *Entry) error { |
||||||
|
hook.Fired = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (hook *ErrorHook) Levels() []Level { |
||||||
|
return []Level{ |
||||||
|
ErrorLevel, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestErrorHookShouldntFireOnInfo(t *testing.T) { |
||||||
|
hook := new(ErrorHook) |
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Hooks.Add(hook) |
||||||
|
log.Info("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, hook.Fired, false) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestErrorHookShouldFireOnError(t *testing.T) { |
||||||
|
hook := new(ErrorHook) |
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Hooks.Add(hook) |
||||||
|
log.Error("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, hook.Fired, true) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
// A hook to be fired when logging on the logging levels returned from
|
||||||
|
// `Levels()` on your implementation of the interface. Note that this is not
|
||||||
|
// fired in a goroutine or a channel with workers, you should handle such
|
||||||
|
// functionality yourself if your call is non-blocking and you don't wish for
|
||||||
|
// the logging calls for levels returned from `Levels()` to block.
|
||||||
|
type Hook interface { |
||||||
|
Levels() []Level |
||||||
|
Fire(*Entry) error |
||||||
|
} |
||||||
|
|
||||||
|
// Internal type for storing the hooks on a logger instance.
|
||||||
|
type LevelHooks map[Level][]Hook |
||||||
|
|
||||||
|
// Add a hook to an instance of logger. This is called with
|
||||||
|
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
|
||||||
|
func (hooks LevelHooks) Add(hook Hook) { |
||||||
|
for _, level := range hook.Levels() { |
||||||
|
hooks[level] = append(hooks[level], hook) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Fire all the hooks for the passed level. Used by `entry.log` to fire
|
||||||
|
// appropriate hooks for a log entry.
|
||||||
|
func (hooks LevelHooks) Fire(level Level, entry *Entry) error { |
||||||
|
for _, hook := range hooks[level] { |
||||||
|
if err := hook.Fire(entry); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
type JSONFormatter struct { |
||||||
|
// TimestampFormat sets the format used for marshaling timestamps.
|
||||||
|
TimestampFormat string |
||||||
|
} |
||||||
|
|
||||||
|
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { |
||||||
|
data := make(Fields, len(entry.Data)+3) |
||||||
|
for k, v := range entry.Data { |
||||||
|
switch v := v.(type) { |
||||||
|
case error: |
||||||
|
// Otherwise errors are ignored by `encoding/json`
|
||||||
|
// https://github.com/Sirupsen/logrus/issues/137
|
||||||
|
data[k] = v.Error() |
||||||
|
default: |
||||||
|
data[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
prefixFieldClashes(data) |
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat |
||||||
|
if timestampFormat == "" { |
||||||
|
timestampFormat = DefaultTimestampFormat |
||||||
|
} |
||||||
|
|
||||||
|
data["time"] = entry.Time.Format(timestampFormat) |
||||||
|
data["msg"] = entry.Message |
||||||
|
data["level"] = entry.Level.String() |
||||||
|
|
||||||
|
serialized, err := json.Marshal(data) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) |
||||||
|
} |
||||||
|
return append(serialized, '\n'), nil |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
|
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestErrorNotLost(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("error", errors.New("wild walrus"))) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
entry := make(map[string]interface{}) |
||||||
|
err = json.Unmarshal(b, &entry) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if entry["error"] != "wild walrus" { |
||||||
|
t.Fatal("Error field not set") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestErrorNotLostOnFieldNotNamedError(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("omg", errors.New("wild walrus"))) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
entry := make(map[string]interface{}) |
||||||
|
err = json.Unmarshal(b, &entry) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if entry["omg"] != "wild walrus" { |
||||||
|
t.Fatal("Error field not set") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestFieldClashWithTime(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("time", "right now!")) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
entry := make(map[string]interface{}) |
||||||
|
err = json.Unmarshal(b, &entry) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if entry["fields.time"] != "right now!" { |
||||||
|
t.Fatal("fields.time not set to original time field") |
||||||
|
} |
||||||
|
|
||||||
|
if entry["time"] != "0001-01-01T00:00:00Z" { |
||||||
|
t.Fatal("time field not set to current time, was: ", entry["time"]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestFieldClashWithMsg(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("msg", "something")) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
entry := make(map[string]interface{}) |
||||||
|
err = json.Unmarshal(b, &entry) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if entry["fields.msg"] != "something" { |
||||||
|
t.Fatal("fields.msg not set to original msg field") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestFieldClashWithLevel(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something")) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
entry := make(map[string]interface{}) |
||||||
|
err = json.Unmarshal(b, &entry) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if entry["fields.level"] != "something" { |
||||||
|
t.Fatal("fields.level not set to original level field") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestJSONEntryEndsWithNewline(t *testing.T) { |
||||||
|
formatter := &JSONFormatter{} |
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something")) |
||||||
|
if err != nil { |
||||||
|
t.Fatal("Unable to format entry: ", err) |
||||||
|
} |
||||||
|
|
||||||
|
if b[len(b)-1] != '\n' { |
||||||
|
t.Fatal("Expected JSON log entry to end with a newline") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,206 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
type Logger struct { |
||||||
|
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
|
||||||
|
// file, or leave it default which is `os.Stderr`. You can also set this to
|
||||||
|
// something more adventorous, such as logging to Kafka.
|
||||||
|
Out io.Writer |
||||||
|
// Hooks for the logger instance. These allow firing events based on logging
|
||||||
|
// levels and log entries. For example, to send errors to an error tracking
|
||||||
|
// service, log to StatsD or dump the core on fatal errors.
|
||||||
|
Hooks LevelHooks |
||||||
|
// All log entries pass through the formatter before logged to Out. The
|
||||||
|
// included formatters are `TextFormatter` and `JSONFormatter` for which
|
||||||
|
// TextFormatter is the default. In development (when a TTY is attached) it
|
||||||
|
// logs with colors, but to a file it wouldn't. You can easily implement your
|
||||||
|
// own that implements the `Formatter` interface, see the `README` or included
|
||||||
|
// formatters for examples.
|
||||||
|
Formatter Formatter |
||||||
|
// The logging level the logger should log at. This is typically (and defaults
|
||||||
|
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
|
||||||
|
// logged. `logrus.Debug` is useful in
|
||||||
|
Level Level |
||||||
|
// Used to sync writing to the log.
|
||||||
|
mu sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
// Creates a new logger. Configuration should be set by changing `Formatter`,
|
||||||
|
// `Out` and `Hooks` directly on the default logger instance. You can also just
|
||||||
|
// instantiate your own:
|
||||||
|
//
|
||||||
|
// var log = &Logger{
|
||||||
|
// Out: os.Stderr,
|
||||||
|
// Formatter: new(JSONFormatter),
|
||||||
|
// Hooks: make(LevelHooks),
|
||||||
|
// Level: logrus.DebugLevel,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// It's recommended to make this a global instance called `log`.
|
||||||
|
func New() *Logger { |
||||||
|
return &Logger{ |
||||||
|
Out: os.Stderr, |
||||||
|
Formatter: new(TextFormatter), |
||||||
|
Hooks: make(LevelHooks), |
||||||
|
Level: InfoLevel, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Adds a field to the log entry, note that you it doesn't log until you call
|
||||||
|
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
|
||||||
|
// If you want multiple fields, use `WithFields`.
|
||||||
|
func (logger *Logger) WithField(key string, value interface{}) *Entry { |
||||||
|
return NewEntry(logger).WithField(key, value) |
||||||
|
} |
||||||
|
|
||||||
|
// Adds a struct of fields to the log entry. All it does is call `WithField` for
|
||||||
|
// each `Field`.
|
||||||
|
func (logger *Logger) WithFields(fields Fields) *Entry { |
||||||
|
return NewEntry(logger).WithFields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Debugf(format string, args ...interface{}) { |
||||||
|
if logger.Level >= DebugLevel { |
||||||
|
NewEntry(logger).Debugf(format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Infof(format string, args ...interface{}) { |
||||||
|
if logger.Level >= InfoLevel { |
||||||
|
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{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warnf(format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Warningf(format string, args ...interface{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warnf(format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Errorf(format string, args ...interface{}) { |
||||||
|
if logger.Level >= ErrorLevel { |
||||||
|
NewEntry(logger).Errorf(format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Fatalf(format string, args ...interface{}) { |
||||||
|
if logger.Level >= FatalLevel { |
||||||
|
NewEntry(logger).Fatalf(format, args...) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Panicf(format string, args ...interface{}) { |
||||||
|
if logger.Level >= PanicLevel { |
||||||
|
NewEntry(logger).Panicf(format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Debug(args ...interface{}) { |
||||||
|
if logger.Level >= DebugLevel { |
||||||
|
NewEntry(logger).Debug(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Info(args ...interface{}) { |
||||||
|
if logger.Level >= InfoLevel { |
||||||
|
NewEntry(logger).Info(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Print(args ...interface{}) { |
||||||
|
NewEntry(logger).Info(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Warn(args ...interface{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warn(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Warning(args ...interface{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warn(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Error(args ...interface{}) { |
||||||
|
if logger.Level >= ErrorLevel { |
||||||
|
NewEntry(logger).Error(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Fatal(args ...interface{}) { |
||||||
|
if logger.Level >= FatalLevel { |
||||||
|
NewEntry(logger).Fatal(args...) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Panic(args ...interface{}) { |
||||||
|
if logger.Level >= PanicLevel { |
||||||
|
NewEntry(logger).Panic(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Debugln(args ...interface{}) { |
||||||
|
if logger.Level >= DebugLevel { |
||||||
|
NewEntry(logger).Debugln(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Infoln(args ...interface{}) { |
||||||
|
if logger.Level >= InfoLevel { |
||||||
|
NewEntry(logger).Infoln(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Println(args ...interface{}) { |
||||||
|
NewEntry(logger).Println(args...) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Warnln(args ...interface{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warnln(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Warningln(args ...interface{}) { |
||||||
|
if logger.Level >= WarnLevel { |
||||||
|
NewEntry(logger).Warnln(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Errorln(args ...interface{}) { |
||||||
|
if logger.Level >= ErrorLevel { |
||||||
|
NewEntry(logger).Errorln(args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Fatalln(args ...interface{}) { |
||||||
|
if logger.Level >= FatalLevel { |
||||||
|
NewEntry(logger).Fatalln(args...) |
||||||
|
} |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) Panicln(args ...interface{}) { |
||||||
|
if logger.Level >= PanicLevel { |
||||||
|
NewEntry(logger).Panicln(args...) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
) |
||||||
|
|
||||||
|
// Fields type, used to pass to `WithFields`.
|
||||||
|
type Fields map[string]interface{} |
||||||
|
|
||||||
|
// Level type
|
||||||
|
type Level uint8 |
||||||
|
|
||||||
|
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
|
||||||
|
func (level Level) String() string { |
||||||
|
switch level { |
||||||
|
case DebugLevel: |
||||||
|
return "debug" |
||||||
|
case InfoLevel: |
||||||
|
return "info" |
||||||
|
case WarnLevel: |
||||||
|
return "warning" |
||||||
|
case ErrorLevel: |
||||||
|
return "error" |
||||||
|
case FatalLevel: |
||||||
|
return "fatal" |
||||||
|
case PanicLevel: |
||||||
|
return "panic" |
||||||
|
} |
||||||
|
|
||||||
|
return "unknown" |
||||||
|
} |
||||||
|
|
||||||
|
// ParseLevel takes a string level and returns the Logrus log level constant.
|
||||||
|
func ParseLevel(lvl string) (Level, error) { |
||||||
|
switch lvl { |
||||||
|
case "panic": |
||||||
|
return PanicLevel, nil |
||||||
|
case "fatal": |
||||||
|
return FatalLevel, nil |
||||||
|
case "error": |
||||||
|
return ErrorLevel, nil |
||||||
|
case "warn", "warning": |
||||||
|
return WarnLevel, nil |
||||||
|
case "info": |
||||||
|
return InfoLevel, nil |
||||||
|
case "debug": |
||||||
|
return DebugLevel, nil |
||||||
|
} |
||||||
|
|
||||||
|
var l Level |
||||||
|
return l, fmt.Errorf("not a valid logrus Level: %q", lvl) |
||||||
|
} |
||||||
|
|
||||||
|
// These are the different logging levels. You can set the logging level to log
|
||||||
|
// on your instance of logger, obtained with `logrus.New()`.
|
||||||
|
const ( |
||||||
|
// PanicLevel level, highest level of severity. Logs and then calls panic with the
|
||||||
|
// message passed to Debug, Info, ...
|
||||||
|
PanicLevel Level = iota |
||||||
|
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
|
||||||
|
// logging level is set to Panic.
|
||||||
|
FatalLevel |
||||||
|
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
|
||||||
|
// Commonly used for hooks to send errors to an error tracking service.
|
||||||
|
ErrorLevel |
||||||
|
// WarnLevel level. Non-critical entries that deserve eyes.
|
||||||
|
WarnLevel |
||||||
|
// InfoLevel level. General operational entries about what's going on inside the
|
||||||
|
// application.
|
||||||
|
InfoLevel |
||||||
|
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
||||||
|
DebugLevel |
||||||
|
) |
||||||
|
|
||||||
|
// Won't compile if StdLogger can't be realized by a log.Logger
|
||||||
|
var ( |
||||||
|
_ StdLogger = &log.Logger{} |
||||||
|
_ StdLogger = &Entry{} |
||||||
|
_ StdLogger = &Logger{} |
||||||
|
) |
||||||
|
|
||||||
|
// StdLogger 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 StdLogger interface { |
||||||
|
Print(...interface{}) |
||||||
|
Printf(string, ...interface{}) |
||||||
|
Println(...interface{}) |
||||||
|
|
||||||
|
Fatal(...interface{}) |
||||||
|
Fatalf(string, ...interface{}) |
||||||
|
Fatalln(...interface{}) |
||||||
|
|
||||||
|
Panic(...interface{}) |
||||||
|
Panicf(string, ...interface{}) |
||||||
|
Panicln(...interface{}) |
||||||
|
} |
@ -0,0 +1,301 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"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 LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) { |
||||||
|
var buffer bytes.Buffer |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &buffer |
||||||
|
logger.Formatter = &TextFormatter{ |
||||||
|
DisableColors: true, |
||||||
|
} |
||||||
|
|
||||||
|
log(logger) |
||||||
|
|
||||||
|
fields := make(map[string]string) |
||||||
|
for _, kv := range strings.Split(buffer.String(), " ") { |
||||||
|
if !strings.Contains(kv, "=") { |
||||||
|
continue |
||||||
|
} |
||||||
|
kvArr := strings.Split(kv, "=") |
||||||
|
key := strings.TrimSpace(kvArr[0]) |
||||||
|
val := kvArr[1] |
||||||
|
if kvArr[1][0] == '"' { |
||||||
|
var err error |
||||||
|
val, err = strconv.Unquote(val) |
||||||
|
assert.NoError(t, err) |
||||||
|
} |
||||||
|
fields[key] = val |
||||||
|
} |
||||||
|
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") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfolnShouldAddSpacesBetweenStrings(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Infoln("test", "test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "test test") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfolnShouldAddSpacesBetweenStringAndNonstring(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Infoln("test", 10) |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "test 10") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfolnShouldAddSpacesBetweenTwoNonStrings(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Infoln(10, 10) |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "10 10") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfoShouldAddSpacesBetweenTwoNonStrings(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Infoln(10, 10) |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "10 10") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfoShouldNotAddSpacesBetweenStringAndNonstring(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Info("test", 10) |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "test10") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestInfoShouldNotAddSpacesBetweenStrings(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.Info("test", "test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "testtest") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWithFieldsShouldAllowAssignments(t *testing.T) { |
||||||
|
var buffer bytes.Buffer |
||||||
|
var fields Fields |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &buffer |
||||||
|
logger.Formatter = new(JSONFormatter) |
||||||
|
|
||||||
|
localLog := logger.WithFields(Fields{ |
||||||
|
"key1": "value1", |
||||||
|
}) |
||||||
|
|
||||||
|
localLog.WithField("key2", "value2").Info("test") |
||||||
|
err := json.Unmarshal(buffer.Bytes(), &fields) |
||||||
|
assert.Nil(t, err) |
||||||
|
|
||||||
|
assert.Equal(t, "value2", fields["key2"]) |
||||||
|
assert.Equal(t, "value1", fields["key1"]) |
||||||
|
|
||||||
|
buffer = bytes.Buffer{} |
||||||
|
fields = Fields{} |
||||||
|
localLog.Info("test") |
||||||
|
err = json.Unmarshal(buffer.Bytes(), &fields) |
||||||
|
assert.Nil(t, err) |
||||||
|
|
||||||
|
_, ok := fields["key2"] |
||||||
|
assert.Equal(t, false, ok) |
||||||
|
assert.Equal(t, "value1", fields["key1"]) |
||||||
|
} |
||||||
|
|
||||||
|
func TestUserSuppliedFieldDoesNotOverwriteDefaults(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.WithField("msg", "hello").Info("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "test") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestUserSuppliedMsgFieldHasPrefix(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.WithField("msg", "hello").Info("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["msg"], "test") |
||||||
|
assert.Equal(t, fields["fields.msg"], "hello") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestUserSuppliedTimeFieldHasPrefix(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.WithField("time", "hello").Info("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["fields.time"], "hello") |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestUserSuppliedLevelFieldHasPrefix(t *testing.T) { |
||||||
|
LogAndAssertJSON(t, func(log *Logger) { |
||||||
|
log.WithField("level", 1).Info("test") |
||||||
|
}, func(fields Fields) { |
||||||
|
assert.Equal(t, fields["level"], "info") |
||||||
|
assert.Equal(t, fields["fields.level"], 1.0) // JSON has floats only
|
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDefaultFieldsAreNotPrefixed(t *testing.T) { |
||||||
|
LogAndAssertText(t, func(log *Logger) { |
||||||
|
ll := log.WithField("herp", "derp") |
||||||
|
ll.Info("hello") |
||||||
|
ll.Info("bye") |
||||||
|
}, func(fields map[string]string) { |
||||||
|
for _, fieldName := range []string{"fields.level", "fields.time", "fields.msg"} { |
||||||
|
if _, ok := fields[fieldName]; ok { |
||||||
|
t.Fatalf("should not have prefixed %q: %v", fieldName, fields) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) { |
||||||
|
|
||||||
|
var buffer bytes.Buffer |
||||||
|
var fields Fields |
||||||
|
|
||||||
|
logger := New() |
||||||
|
logger.Out = &buffer |
||||||
|
logger.Formatter = new(JSONFormatter) |
||||||
|
|
||||||
|
llog := logger.WithField("context", "eating raw fish") |
||||||
|
|
||||||
|
llog.Info("looks delicious") |
||||||
|
|
||||||
|
err := json.Unmarshal(buffer.Bytes(), &fields) |
||||||
|
assert.NoError(t, err, "should have decoded first message") |
||||||
|
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields") |
||||||
|
assert.Equal(t, fields["msg"], "looks delicious") |
||||||
|
assert.Equal(t, fields["context"], "eating raw fish") |
||||||
|
|
||||||
|
buffer.Reset() |
||||||
|
|
||||||
|
llog.Warn("omg it is!") |
||||||
|
|
||||||
|
err = json.Unmarshal(buffer.Bytes(), &fields) |
||||||
|
assert.NoError(t, err, "should have decoded second message") |
||||||
|
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields") |
||||||
|
assert.Equal(t, fields["msg"], "omg it is!") |
||||||
|
assert.Equal(t, fields["context"], "eating raw fish") |
||||||
|
assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry") |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func TestConvertLevelToString(t *testing.T) { |
||||||
|
assert.Equal(t, "debug", DebugLevel.String()) |
||||||
|
assert.Equal(t, "info", InfoLevel.String()) |
||||||
|
assert.Equal(t, "warning", WarnLevel.String()) |
||||||
|
assert.Equal(t, "error", ErrorLevel.String()) |
||||||
|
assert.Equal(t, "fatal", FatalLevel.String()) |
||||||
|
assert.Equal(t, "panic", PanicLevel.String()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestParseLevel(t *testing.T) { |
||||||
|
l, err := ParseLevel("panic") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, PanicLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("fatal") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, FatalLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("error") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, ErrorLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("warn") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, WarnLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("warning") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, WarnLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("info") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, InfoLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("debug") |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, DebugLevel, l) |
||||||
|
|
||||||
|
l, err = ParseLevel("invalid") |
||||||
|
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetSetLevelRace(t *testing.T) { |
||||||
|
wg := sync.WaitGroup{} |
||||||
|
for i := 0; i < 100; i++ { |
||||||
|
wg.Add(1) |
||||||
|
go func(i int) { |
||||||
|
defer wg.Done() |
||||||
|
if i%2 == 0 { |
||||||
|
SetLevel(InfoLevel) |
||||||
|
} else { |
||||||
|
GetLevel() |
||||||
|
} |
||||||
|
}(i) |
||||||
|
|
||||||
|
} |
||||||
|
wg.Wait() |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
// +build darwin freebsd openbsd netbsd dragonfly
|
||||||
|
|
||||||
|
package logrus |
||||||
|
|
||||||
|
import "syscall" |
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA |
||||||
|
|
||||||
|
type Termios syscall.Termios |
@ -0,0 +1,12 @@ |
|||||||
|
// Based on ssh/terminal:
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package logrus |
||||||
|
|
||||||
|
import "syscall" |
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TCGETS |
||||||
|
|
||||||
|
type Termios syscall.Termios |
@ -0,0 +1,21 @@ |
|||||||
|
// Based on ssh/terminal:
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build linux darwin freebsd openbsd netbsd dragonfly
|
||||||
|
|
||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"syscall" |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal() bool { |
||||||
|
fd := syscall.Stdout |
||||||
|
var termios Termios |
||||||
|
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) |
||||||
|
return err == 0 |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
// Based on ssh/terminal:
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"syscall" |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
var kernel32 = syscall.NewLazyDLL("kernel32.dll") |
||||||
|
|
||||||
|
var ( |
||||||
|
procGetConsoleMode = kernel32.NewProc("GetConsoleMode") |
||||||
|
) |
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal() bool { |
||||||
|
fd := syscall.Stdout |
||||||
|
var st uint32 |
||||||
|
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) |
||||||
|
return r != 0 && e == 0 |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"runtime" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
nocolor = 0 |
||||||
|
red = 31 |
||||||
|
green = 32 |
||||||
|
yellow = 33 |
||||||
|
blue = 34 |
||||||
|
gray = 37 |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
baseTimestamp time.Time |
||||||
|
isTerminal bool |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
baseTimestamp = time.Now() |
||||||
|
isTerminal = IsTerminal() |
||||||
|
} |
||||||
|
|
||||||
|
func miniTS() int { |
||||||
|
return int(time.Since(baseTimestamp) / time.Second) |
||||||
|
} |
||||||
|
|
||||||
|
type TextFormatter struct { |
||||||
|
// Set to true to bypass checking for a TTY before outputting colors.
|
||||||
|
ForceColors bool |
||||||
|
|
||||||
|
// Force disabling colors.
|
||||||
|
DisableColors bool |
||||||
|
|
||||||
|
// Disable timestamp logging. useful when output is redirected to logging
|
||||||
|
// system that already adds timestamps.
|
||||||
|
DisableTimestamp bool |
||||||
|
|
||||||
|
// Enable logging the full timestamp when a TTY is attached instead of just
|
||||||
|
// the time passed since beginning of execution.
|
||||||
|
FullTimestamp bool |
||||||
|
|
||||||
|
// TimestampFormat to use for display when a full timestamp is printed
|
||||||
|
TimestampFormat string |
||||||
|
|
||||||
|
// The fields are sorted by default for a consistent output. For applications
|
||||||
|
// that log extremely frequently and don't use the JSON formatter this may not
|
||||||
|
// be desired.
|
||||||
|
DisableSorting bool |
||||||
|
} |
||||||
|
|
||||||
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { |
||||||
|
var keys []string = make([]string, 0, len(entry.Data)) |
||||||
|
for k := range entry.Data { |
||||||
|
keys = append(keys, k) |
||||||
|
} |
||||||
|
|
||||||
|
if !f.DisableSorting { |
||||||
|
sort.Strings(keys) |
||||||
|
} |
||||||
|
|
||||||
|
b := &bytes.Buffer{} |
||||||
|
|
||||||
|
prefixFieldClashes(entry.Data) |
||||||
|
|
||||||
|
isColorTerminal := isTerminal && (runtime.GOOS != "windows") |
||||||
|
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors |
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat |
||||||
|
if timestampFormat == "" { |
||||||
|
timestampFormat = DefaultTimestampFormat |
||||||
|
} |
||||||
|
if isColored { |
||||||
|
f.printColored(b, entry, keys, timestampFormat) |
||||||
|
} else { |
||||||
|
if !f.DisableTimestamp { |
||||||
|
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) |
||||||
|
} |
||||||
|
f.appendKeyValue(b, "level", entry.Level.String()) |
||||||
|
f.appendKeyValue(b, "msg", entry.Message) |
||||||
|
for _, key := range keys { |
||||||
|
f.appendKeyValue(b, key, entry.Data[key]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
b.WriteByte('\n') |
||||||
|
return b.Bytes(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { |
||||||
|
var levelColor int |
||||||
|
switch entry.Level { |
||||||
|
case DebugLevel: |
||||||
|
levelColor = gray |
||||||
|
case WarnLevel: |
||||||
|
levelColor = yellow |
||||||
|
case ErrorLevel, FatalLevel, PanicLevel: |
||||||
|
levelColor = red |
||||||
|
default: |
||||||
|
levelColor = blue |
||||||
|
} |
||||||
|
|
||||||
|
levelText := strings.ToUpper(entry.Level.String())[0:4] |
||||||
|
|
||||||
|
if !f.FullTimestamp { |
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) |
||||||
|
} else { |
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) |
||||||
|
} |
||||||
|
for _, k := range keys { |
||||||
|
v := entry.Data[k] |
||||||
|
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func needsQuoting(text string) bool { |
||||||
|
for _, ch := range text { |
||||||
|
if !((ch >= 'a' && ch <= 'z') || |
||||||
|
(ch >= 'A' && ch <= 'Z') || |
||||||
|
(ch >= '0' && ch <= '9') || |
||||||
|
ch == '-' || ch == '.') { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { |
||||||
|
|
||||||
|
b.WriteString(key) |
||||||
|
b.WriteByte('=') |
||||||
|
|
||||||
|
switch value := value.(type) { |
||||||
|
case string: |
||||||
|
if needsQuoting(value) { |
||||||
|
b.WriteString(value) |
||||||
|
} else { |
||||||
|
fmt.Fprintf(b, "%q", value) |
||||||
|
} |
||||||
|
case error: |
||||||
|
errmsg := value.Error() |
||||||
|
if needsQuoting(errmsg) { |
||||||
|
b.WriteString(errmsg) |
||||||
|
} else { |
||||||
|
fmt.Fprintf(b, "%q", value) |
||||||
|
} |
||||||
|
default: |
||||||
|
fmt.Fprint(b, value) |
||||||
|
} |
||||||
|
|
||||||
|
b.WriteByte(' ') |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"errors" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func TestQuoting(t *testing.T) { |
||||||
|
tf := &TextFormatter{DisableColors: true} |
||||||
|
|
||||||
|
checkQuoting := func(q bool, value interface{}) { |
||||||
|
b, _ := tf.Format(WithField("test", value)) |
||||||
|
idx := bytes.Index(b, ([]byte)("test=")) |
||||||
|
cont := bytes.Contains(b[idx+5:], []byte{'"'}) |
||||||
|
if cont != q { |
||||||
|
if q { |
||||||
|
t.Errorf("quoting expected for: %#v", value) |
||||||
|
} else { |
||||||
|
t.Errorf("quoting not expected for: %#v", value) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
checkQuoting(false, "abcd") |
||||||
|
checkQuoting(false, "v1.0") |
||||||
|
checkQuoting(false, "1234567890") |
||||||
|
checkQuoting(true, "/foobar") |
||||||
|
checkQuoting(true, "x y") |
||||||
|
checkQuoting(true, "x,y") |
||||||
|
checkQuoting(false, errors.New("invalid")) |
||||||
|
checkQuoting(true, errors.New("invalid argument")) |
||||||
|
} |
||||||
|
|
||||||
|
func TestTimestampFormat(t *testing.T) { |
||||||
|
checkTimeStr := func(format string) { |
||||||
|
customFormatter := &TextFormatter{DisableColors: true, TimestampFormat: format} |
||||||
|
customStr, _ := customFormatter.Format(WithField("test", "test")) |
||||||
|
timeStart := bytes.Index(customStr, ([]byte)("time=")) |
||||||
|
timeEnd := bytes.Index(customStr, ([]byte)("level=")) |
||||||
|
timeStr := customStr[timeStart+5 : timeEnd-1] |
||||||
|
if timeStr[0] == '"' && timeStr[len(timeStr)-1] == '"' { |
||||||
|
timeStr = timeStr[1 : len(timeStr)-1] |
||||||
|
} |
||||||
|
if format == "" { |
||||||
|
format = time.RFC3339 |
||||||
|
} |
||||||
|
_, e := time.Parse(format, (string)(timeStr)) |
||||||
|
if e != nil { |
||||||
|
t.Errorf("time string \"%s\" did not match provided time format \"%s\": %s", timeStr, format, e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
checkTimeStr("2006-01-02T15:04:05.000000000Z07:00") |
||||||
|
checkTimeStr("Mon Jan _2 15:04:05 2006") |
||||||
|
checkTimeStr("") |
||||||
|
} |
||||||
|
|
||||||
|
// TODO add tests for sorting etc., this requires a parser for the text
|
||||||
|
// formatter output.
|
@ -0,0 +1,31 @@ |
|||||||
|
package logrus |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"io" |
||||||
|
"runtime" |
||||||
|
) |
||||||
|
|
||||||
|
func (logger *Logger) Writer() *io.PipeWriter { |
||||||
|
reader, writer := io.Pipe() |
||||||
|
|
||||||
|
go logger.writerScanner(reader) |
||||||
|
runtime.SetFinalizer(writer, writerFinalizer) |
||||||
|
|
||||||
|
return writer |
||||||
|
} |
||||||
|
|
||||||
|
func (logger *Logger) writerScanner(reader *io.PipeReader) { |
||||||
|
scanner := bufio.NewScanner(reader) |
||||||
|
for scanner.Scan() { |
||||||
|
logger.Print(scanner.Text()) |
||||||
|
} |
||||||
|
if err := scanner.Err(); err != nil { |
||||||
|
logger.Errorf("Error while reading from Writer: %s", err) |
||||||
|
} |
||||||
|
reader.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func writerFinalizer(writer *io.PipeWriter) { |
||||||
|
writer.Close() |
||||||
|
} |
Loading…
Reference in new issue