Skip to content

Commit

Permalink
A new MultiWriters design that doesn't require parsing levels (#8)
Browse files Browse the repository at this point in the history
* A new implementation of MultiWriters

* Add benchmarks for the new and old multi writers solution and optimize CombineWriters

* Use more general name

* All messages to be written to the InfoWriter

* User an interface

* Bugfix

* Use the LeveledWriter interface for better integration with existing code
  • Loading branch information
suzaku committed Aug 15, 2020
1 parent 6fbe829 commit cba483d
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 2 deletions.
8 changes: 7 additions & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type Logger struct {

// Writer specifies the writer of output. It uses os.Stderr in if empty.
Writer io.Writer

// LeveledWriter decides which writers to write for each level.
// When this field is set, the Writer field is ignored.
LeveledWriter LeveledWriter
}

const (
Expand Down Expand Up @@ -283,7 +287,9 @@ func (l *Logger) header(level Level) *Event {
e.exit = false
e.panic = true
}
if l.Writer != nil {
if l.LeveledWriter != nil {
e.w = wrapLeveledWriter{Level: level, LeveledWriter: l.LeveledWriter}
} else if l.Writer != nil {
e.w = l.Writer
} else {
e.w = os.Stderr
Expand Down
57 changes: 56 additions & 1 deletion multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import (
"io"
)

// LeveledWriter is an interface that wraps WriteAtLevel.
type LeveledWriter interface {
// WriteAtLevel decides which writers to use by checking the specified Level.
WriteAtLevel(Level, []byte) (int, error)
}

// MultiWriter is an io.WriteCloser that log to different writers by different levels
type MultiWriter struct {
// InfoWriter specifies the level large than info logs writes to
Expand Down Expand Up @@ -33,7 +39,7 @@ type MultiWriter struct {
ParseLevel func([]byte) Level
}

// Close implements io.Closer, and closes the underlying Writers.
// Close implements io.Closer, and closes the underlying LeveledWriter.
func (w *MultiWriter) Close() (err error) {
for _, writer := range []io.Writer{
w.InfoWriter,
Expand All @@ -56,6 +62,42 @@ func (w *MultiWriter) Close() (err error) {

var levelBegin = []byte(`"level":"`)

// WriteAtLevel implements LeveledWriter.
func (w *MultiWriter) WriteAtLevel(level Level, p []byte) (n int, err error) {
var err1 error
switch level {
case noLevel, PanicLevel, FatalLevel, ErrorLevel:
if w.ErrorWriter != nil {
n, err1 = w.ErrorWriter.Write(p)
if err1 != nil && err == nil {
err = err1
}
}
fallthrough
case WarnLevel:
if w.WarnWriter != nil {
n, err1 = w.WarnWriter.Write(p)
if err1 != nil && err == nil {
err = err1
}
}
fallthrough
default:
if w.InfoWriter != nil {
n, err1 = w.InfoWriter.Write(p)
if err1 != nil && err == nil {
err = err1
}
}
}

if w.StderrWriter != nil && level >= w.StderrLevel {
w.StderrWriter.Write(p)
}

return
}

// Write implements io.Writer.
func (w *MultiWriter) Write(p []byte) (n int, err error) {
var level = noLevel
Expand Down Expand Up @@ -132,3 +174,16 @@ func (w *MultiWriter) Write(p []byte) (n int, err error) {

return
}

// wrapLeveledWriter wraps a LeveledWriter to implement io.Writer.
type wrapLeveledWriter struct {
Level Level
LeveledWriter LeveledWriter
}

// Write implements io.Writer.
func (w wrapLeveledWriter) Write(p []byte) (int, error) {
return w.LeveledWriter.WriteAtLevel(w.Level, p)
}

var _ io.Writer = wrapLeveledWriter{}
111 changes: 111 additions & 0 deletions multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,49 @@ import (
"testing"
)

func TestLoggerWithLeveledWriters(t *testing.T) {
// TODO: Use mock writers to validate that the logs are routed correctly
w := &MultiWriter{
InfoWriter: &FileWriter{
Filename: "file-info.log",
},
WarnWriter: &FileWriter{
Filename: "file-warn.log",
},
ErrorWriter: &FileWriter{
Filename: "file-error.log",
},
}
logger := Logger{
Level: InfoLevel,
Caller: 1,
LeveledWriter: w,
}
assertNLogs := func(want int) {
matches, _ := filepath.Glob("file-*.*.log")
if len(matches) != want {
t.Fatalf("filepath glob return %+v number mismatch, got %+v want %+v", matches, len(matches), want)
}
}

logger.Info().Int("id", 42).Msg("I'm loving it.")
assertNLogs(1)

logger.Warn().Int("id", 43).Msg("I double dare you.")
assertNLogs(2)

logger.Error().Str("action", "cleanup").Msg("World")
assertNLogs(3)

matches, _ := filepath.Glob("file-*.log")
for i := range matches {
err := os.Remove(matches[i])
if err != nil {
t.Fatalf("os remove %s error: %+v", matches[i], err)
}
}
}

func TestMultiWriter(t *testing.T) {
w := &MultiWriter{
InfoWriter: &FileWriter{
Expand Down Expand Up @@ -172,3 +215,71 @@ func TestMultiWriterLevel(t *testing.T) {
t.Errorf("test close mutli writer error: %+v", err)
}
}

func BenchmarkNewMultiWriter(b *testing.B) {
w := &MultiWriter{
InfoWriter: &FileWriter{
Filename: "file-info.log",
},
WarnWriter: &FileWriter{
Filename: "file-warn.log",
},
ErrorWriter: &FileWriter{
Filename: "file-error.log",
},
}
logger := Logger{
Level: InfoLevel,
Caller: 1,
LeveledWriter: w,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Info().Int("id", 42).Msg("I'm loving it.")
logger.Warn().Int("id", 43).Msg("I double dare you.")
logger.Error().Str("action", "cleanup").Msg("World")
}
b.StopTimer()
matches, _ := filepath.Glob("file-*.log")
for i := range matches {
err := os.Remove(matches[i])
if err != nil {
b.Fatalf("os remove %s error: %+v", matches[i], err)
}
}
}

func BenchmarkOldMultiWriter(b *testing.B) {
w := &MultiWriter{
InfoWriter: &FileWriter{
Filename: "file-info.log",
},
WarnWriter: &FileWriter{
Filename: "file-warn.log",
},
ErrorWriter: &FileWriter{
Filename: "file-error.log",
},
}
logger := Logger{
Level: InfoLevel,
Caller: 1,
Writer: w,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Info().Int("id", 42).Strs("names", []string{"apple", "orange"}).Msg("I'm loving it.")
logger.Warn().Int("id", 43).Msg("I double dare you.")
logger.Error().Str("action", "cleanup").Msg("World")
}
b.StopTimer()
matches, _ := filepath.Glob("file-*.log")
for i := range matches {
err := os.Remove(matches[i])
if err != nil {
b.Fatalf("os remove %s error: %+v", matches[i], err)
}
}
}

0 comments on commit cba483d

Please sign in to comment.