Skip to content

Commit

Permalink
Add log FormatOutput support, allowing to customize the format and ou…
Browse files Browse the repository at this point in the history
…tputter of each log.
  • Loading branch information
edoger committed May 15, 2023
1 parent e556664 commit b6d06dc
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 14 deletions.
53 changes: 53 additions & 0 deletions formatter.go
Expand Up @@ -16,6 +16,7 @@ package logger

import (
"bytes"
"io"
)

// Formatter interface defines a standard log formatter.
Expand Down Expand Up @@ -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)
}
75 changes: 74 additions & 1 deletion formatter_test.go
Expand Up @@ -14,7 +14,12 @@

package logger

import "testing"
import (
"bytes"
"errors"
"io"
"testing"
)

func TestUnimplementedFormatter(t *testing.T) {
var v any = new(UnimplementedFormatter)
Expand All @@ -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)
}
}
44 changes: 32 additions & 12 deletions log.go
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
21 changes: 20 additions & 1 deletion logger.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit b6d06dc

Please sign in to comment.