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