diff --git a/README.md b/README.md index 5f656c3e1..f2a6ed893 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,10 @@ func init() { // Can be any io.Writer, see below for File example log.SetOutput(os.Stdout) + // Output log messages with error severity or above to a separate io.Writer + // Can be any io.Writer + log.SetErrOutput(os.Stderr) + // Only log the warning severity or above. log.SetLevel(log.WarnLevel) } diff --git a/entry.go b/entry.go index 1fad45e08..9f1e91571 100644 --- a/entry.go +++ b/entry.go @@ -114,7 +114,11 @@ func (entry Entry) log(level Level, msg string) { entry.Logger.mu.Unlock() } else { entry.Logger.mu.Lock() - _, err = entry.Logger.Out.Write(serialized) + if entry.Logger.ErrOut != nil && entry.Level <= ErrorLevel { + _, err = entry.Logger.ErrOut.Write(serialized) + } else { + _, err = entry.Logger.Out.Write(serialized) + } if err != nil { fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) } diff --git a/exported.go b/exported.go index 013183eda..7b4429d09 100644 --- a/exported.go +++ b/exported.go @@ -20,6 +20,14 @@ func SetOutput(out io.Writer) { std.Out = out } +// SetErrOutput sets an optional, dedidated io.Writer for log messages with +// error severity or above +func SetErrOutput(errOut io.Writer) { + std.mu.Lock() + defer std.mu.Unlock() + std.ErrOut = errOut +} + // SetFormatter sets the standard logger formatter. func SetFormatter(formatter Formatter) { std.mu.Lock() diff --git a/formatter.go b/formatter.go index b183ff5b1..c96245b77 100644 --- a/formatter.go +++ b/formatter.go @@ -13,7 +13,8 @@ const defaultTimestampFormat = time.RFC3339 // // 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`. +// logged to `logger.Out` (or `logger.ErrOut`, if configured and warranted by +// the severity). type Formatter interface { Format(*Entry) ([]byte, error) } diff --git a/logger.go b/logger.go index fdaf8a653..7851d955f 100644 --- a/logger.go +++ b/logger.go @@ -12,16 +12,19 @@ type Logger struct { // 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 + // An optional, separate io.Writer for log messages with error severity or + // above + ErrOut 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. + // All log entries pass through the formatter before being logged to Out or + // ErrOut. 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 @@ -55,8 +58,8 @@ func (mw *MutexWrap) Disable() { } // 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: +// `Out`, `ErrOut`, and `Hooks` directly on the default logger instance. You can +// also just instantiate your own: // // var log = &Logger{ // Out: os.Stderr, diff --git a/logrus_test.go b/logrus_test.go index 78cbc2825..132cdd314 100644 --- a/logrus_test.go +++ b/logrus_test.go @@ -384,3 +384,56 @@ func TestEntryWriter(t *testing.T) { assert.Equal(t, fields["foo"], "bar") assert.Equal(t, fields["level"], "warning") } + +func TestLogsWithoutSplitStream(t *testing.T) { + var buffer bytes.Buffer + + logger := New() + logger.Out = &buffer + logger.Level = DebugLevel + + assert.Nil(t, logger.ErrOut) + + bufferLen := 0 + assert.Equal(t, bufferLen, buffer.Len()) + // Assert that this one buffer grows as we log, regardless of severity + for _, logCall := range []func(...interface{}){ + logger.Error, + logger.Warn, + logger.Info, + logger.Debug, + } { + logCall("foo") + assert.True(t, buffer.Len() > bufferLen) + bufferLen = buffer.Len() + } +} + +func TestLogsWithSplitStream(t *testing.T) { + var buffer, errBuffer bytes.Buffer + + logger := New() + logger.Out = &buffer + logger.ErrOut = &errBuffer + logger.Level = DebugLevel + + bufferLen := 0 + errBufferLen := 0 + assert.Equal(t, bufferLen, buffer.Len()) + assert.Equal(t, errBufferLen, buffer.Len()) + // Assert that ONLY buffer grows as we log with severity below error + for _, logCall := range []func(...interface{}){ + logger.Warn, + logger.Info, + logger.Debug, + } { + logCall("foo") + assert.True(t, buffer.Len() > bufferLen) + assert.Equal(t, errBufferLen, errBuffer.Len()) + bufferLen = buffer.Len() + } + // Assert that ONLY errBuffer grows as we log with error severity + logger.Error("foo") + assert.True(t, errBuffer.Len() > errBufferLen) + assert.Equal(t, bufferLen, buffer.Len()) +} diff --git a/text_formatter.go b/text_formatter.go index be412aa94..cb057fdc7 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -58,14 +58,18 @@ type TextFormatter struct { QuoteEmptyFields bool // Whether the logger's out is to a terminal - isTerminal bool + isOutTerminal bool + + // Whether the logger's errOut is to a terminal + isErrOutTerminal bool sync.Once } func (f *TextFormatter) init(entry *Entry) { if entry.Logger != nil { - f.isTerminal = f.checkIfTerminal(entry.Logger.Out) + f.isOutTerminal = f.checkIfTerminal(entry.Logger.Out) + f.isErrOutTerminal = f.checkIfTerminal(entry.Logger.ErrOut) } } @@ -99,7 +103,12 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { f.Do(func() { f.init(entry) }) - isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors + var isColored bool + if entry.Logger.ErrOut != nil && entry.Level <= ErrorLevel { + isColored = (f.ForceColors || f.isErrOutTerminal) && !f.DisableColors + } else { + isColored = (f.ForceColors || f.isOutTerminal) && !f.DisableColors + } timestampFormat := f.TimestampFormat if timestampFormat == "" {