From b6d06dc0f2b747d0ee8e48bdbe93bebd736dc8bf Mon Sep 17 00:00:00 2001 From: Qingshan Luo Date: Tue, 16 May 2023 00:54:54 +0800 Subject: [PATCH] Add log FormatOutput support, allowing to customize the format and outputter of each log. --- formatter.go | 53 +++++++++++++++++++++++++++++++++ formatter_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++- log.go | 44 +++++++++++++++++++-------- logger.go | 21 ++++++++++++- 4 files changed, 179 insertions(+), 14 deletions(-) diff --git a/formatter.go b/formatter.go index 2d3189f..2369b70 100644 --- a/formatter.go +++ b/formatter.go @@ -16,6 +16,7 @@ package logger import ( "bytes" + "io" ) // Formatter interface defines a standard log formatter. @@ -47,3 +48,55 @@ type UnimplementedFormatter struct{} func (*UnimplementedFormatter) Format(_ Entity, _ *bytes.Buffer) error { return nil } + +// FormatOutput defines the format output of the logger. +type FormatOutput interface { + // Format formats the given log entity and returns the writer to which the log needs to be written. + // If the error returned is not empty, the log will be discarded and the registered log hook will + // not be triggered. We will output the error information to os.Stderr. + // If the returned log writer is nil, the default log writer is used. + Format(e Entity, b *bytes.Buffer) (io.Writer, error) +} + +// FormatOutputFunc type defines a log format output in the form of a function. +type FormatOutputFunc func(e Entity, b *bytes.Buffer) (io.Writer, error) + +// Format formats the given log entity and returns the writer to which the log needs to be written. +func (f FormatOutputFunc) Format(e Entity, b *bytes.Buffer) (io.Writer, error) { + return f(e, b) +} + +// NewFormatOutput creates a log format output instance from the given formatter and writer. +func NewFormatOutput(f Formatter, w io.Writer) FormatOutput { + return &formatOutput{f, w} +} + +// This is the built-in log format output wrapper. +type formatOutput struct { + f Formatter + w io.Writer +} + +// Format formats the given log entity and returns the writer to which the log needs to be written. +func (w *formatOutput) Format(e Entity, b *bytes.Buffer) (io.Writer, error) { + return w.w, w.f.Format(e, b) +} + +// NewLevelPriorityFormatOutput creates a log level priority format output instance from the given parameters. +func NewLevelPriorityFormatOutput(high, low FormatOutput) FormatOutput { + return &levelPriorityFormatOutput{high, low} +} + +// This is the built-in log level priority format output wrapper. +type levelPriorityFormatOutput struct { + high FormatOutput + low FormatOutput +} + +// Format formats the given log entity and returns the writer to which the log needs to be written. +func (w *levelPriorityFormatOutput) Format(e Entity, b *bytes.Buffer) (io.Writer, error) { + if IsHighPriorityLevel(e.Level()) { + return w.high.Format(e, b) + } + return w.low.Format(e, b) +} diff --git a/formatter_test.go b/formatter_test.go index ffb33da..71193e1 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -14,7 +14,12 @@ package logger -import "testing" +import ( + "bytes" + "errors" + "io" + "testing" +) func TestUnimplementedFormatter(t *testing.T) { var v any = new(UnimplementedFormatter) @@ -27,3 +32,71 @@ func TestUnimplementedFormatter(t *testing.T) { t.Fatal("UnimplementedFormatter: not is Formatter") } } + +func TestFormatOutputFunc(t *testing.T) { + f := FormatOutputFunc(func(_ Entity, _ *bytes.Buffer) (io.Writer, error) { + return nil, errors.New("test") + }) + if _, err := f.Format(nil, nil); err == nil { + t.Fatal("FormatOutputFunc.Format(): nil error") + } else { + if err.Error() != "test" { + t.Fatalf("FormatOutputFunc.Format(): %s", err) + } + } +} + +func TestFormatOutput(t *testing.T) { + w := new(bytes.Buffer) + f := NewFormatOutput(FormatterFunc(func(e Entity, b *bytes.Buffer) error { + b.WriteString(e.Message()) + return nil + }), w) + if f == nil { + t.Fatal("NewFormatOutput(): nil") + } + + o := New("test") + o.SetFormatOutput(f) + o.SetLevel(TraceLevel) + + o.Info("test") + + if got := w.String(); got != "test" { + t.Fatalf("FormatOutput: %s", got) + } +} + +func TestLevelPriorityFormatOutput(t *testing.T) { + w1 := new(bytes.Buffer) + w2 := new(bytes.Buffer) + f1 := NewFormatOutput(FormatterFunc(func(e Entity, b *bytes.Buffer) error { + b.WriteString(e.Message()) + return nil + }), w1) + f2 := NewFormatOutput(FormatterFunc(func(e Entity, b *bytes.Buffer) error { + b.WriteString(e.Message()) + return nil + }), w2) + if f1 == nil || f2 == nil { + t.Fatal("NewFormatOutput(): nil") + } + f := NewLevelPriorityFormatOutput(f1, f2) + if f == nil { + t.Fatal("NewLevelPriorityFormatOutput(): nil") + } + + o := New("test") + o.SetFormatOutput(f) + o.SetLevel(TraceLevel) + + o.Info("info") + o.Error("error") + + if got := w1.String(); got != "error" { + t.Fatalf("LevelPriorityFormatOutput: %s", got) + } + if got := w2.String(); got != "info" { + t.Fatalf("LevelPriorityFormatOutput: %s", got) + } +} diff --git a/log.go b/log.go index d0d7c90..237703d 100644 --- a/log.go +++ b/log.go @@ -215,6 +215,7 @@ type core struct { name string level uint32 formatter Formatter + formatOutput FormatOutput writer io.Writer levelWriter map[Level]io.Writer pool sync.Pool @@ -400,20 +401,28 @@ func (o *log) record(level Level, message string) { entity.stack = internal.GetStack(o.core.stackPrefixes) } - if err := o.core.formatter.Format(entity, &entity.buffer); err != nil { - // When the format log fails, we terminate the logging and report the error. - internal.EchoError("(%s) Failed to format log: %s", o.core.name, err) + var ( + err error + w io.Writer + ) + if o.core.formatOutput == nil { + err = o.core.formatter.Format(entity, entity.Buffer()) } else { + w, err = o.core.formatOutput.Format(entity, entity.Buffer()) + } + if err == nil { if o.core.enableHooks { err = o.core.hooks.Fire(entity) if err != nil { internal.EchoError("(%s) Failed to fire log hook: %s", o.core.name, err) } } - err = o.write(entity) - if err != nil { + if err = o.write(entity, w); err != nil { internal.EchoError("(%s) Failed to write log: %s", o.core.name, err) } + } else { + // When the format log fails, we terminate the logging and report the error. + internal.EchoError("(%s) Failed to format log: %s", o.core.name, err) } if level < ErrorLevel { @@ -426,20 +435,31 @@ func (o *log) record(level Level, message string) { } } -// Write the current log. -func (o *log) write(entity *logEntity) (err error) { - var w io.Writer - if writer, found := o.core.levelWriter[entity.level]; found && writer != nil { - w = writer - } else { - w = o.core.writer +// Get the log writer. +func (o *log) getWriter(entity *logEntity) io.Writer { + if len(o.core.levelWriter) > 0 { + w, found := o.core.levelWriter[entity.level] + if found && w != nil { + return w + } } + return o.core.writer +} + +// Write the current log. +func (o *log) write(entity *logEntity, w io.Writer) (err error) { if o.core.interceptor == nil { // When there is no interceptor, make sure that the log written is not empty. if entity.Size() > 0 { + if w == nil { + w = o.getWriter(entity) + } _, err = w.Write(entity.Bytes()) } } else { + if w == nil { + w = o.getWriter(entity) + } _, err = o.core.interceptor(entity, w) } return diff --git a/logger.go b/logger.go index 4e4d3da..bf229a1 100644 --- a/logger.go +++ b/logger.go @@ -80,6 +80,12 @@ type Logger interface { // If the given log formatter is nil, we will record the log in JSON format. SetFormatter(Formatter) Logger + // SetFormatOutput sets the log format output. + // After setting the format output, the format and output of the logger will be controlled by this structure, + // and the bound log output and log level output will no longer be used. + // If format output needs to be disabled, set to nil and the logger will back to the original behavior. + SetFormatOutput(FormatOutput) Logger + // SetDefaultTimeFormat sets the default log time format for the current logger. // If the given time format is empty string, internal.DefaultTimeFormat is used. SetDefaultTimeFormat(string) Logger @@ -171,7 +177,11 @@ func (o *logger) SetOutput(w io.Writer) Logger { // The level output writer is used to write log data of a given level. // If the given writer is nil, the level writer will be disabled. func (o *logger) SetLevelOutput(level Level, w io.Writer) Logger { - o.core.levelWriter[level] = w + if w == nil { + delete(o.core.levelWriter, level) + } else { + o.core.levelWriter[level] = w + } return o } @@ -240,6 +250,15 @@ func (o *logger) SetFormatter(formatter Formatter) Logger { return o } +// SetFormatOutput sets the log format output. +// After setting the format output, the format and output of the logger will be controlled by this structure, +// and the bound log output and log level output will no longer be used. +// If format output needs to be disabled, set to nil and the logger will back to the original behavior. +func (o *logger) SetFormatOutput(fo FormatOutput) Logger { + o.core.formatOutput = fo + return o +} + // SetDefaultTimeFormat sets the default log time format for the current logger. // If the given time format is empty string, internal.DefaultTimeFormat is used. func (o *logger) SetDefaultTimeFormat(format string) Logger {