From e07ba15fe1d4f16db3a1021c42100b247076ca5c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 7 Sep 2020 01:02:37 +0300 Subject: [PATCH] introduce the Formatter interface (one level higher than the Handler) --- HISTORY.md | 9 ++- README.md | 26 ++++++- _examples/customize-output/main.go | 4 +- formatter.go | 80 ++++++++++++++++++++++ golog.go | 15 ++++ json.go | 37 ---------- logger.go | 106 +++++++++++++++++++++++++---- 7 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 formatter.go delete mode 100644 json.go diff --git a/HISTORY.md b/HISTORY.md index 874b3d9..71cda87 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,15 @@ +## Mo 07 September | v0.1.5 + +Introduce the [Formatter](https://github.com/kataras/golog/blob/master/formatter.go) interface. [Example](https://github.com/kataras/golog/tree/master/_examples/customize-output). + +- Add `Logger.RegisterFormatter(Formatter)` to register a custom `Formatter`. +- Add `Logger.SetFormat(formatter string, opts ...interface{})` to set the default formatter for all log levels. +- Add `Logger.SetLevelFormat(levelName string, formatter string, opts ...interface{})` to change the output format for the given "levelName". + ## Su 06 September | v0.1.3 and v0.1.4 - Add `Logger.SetLevelOutput(levelName string, w io.Writer)` to customize the writer per level. - Add `Logger.GetLevelOutput(levelName string) io.Writer` to get the leveled output or the default one. -- Add `JSON(indent string) Handler` as a helper for JSON format: `Logger.Handle(golog.JSON(" "))`. ## Sa 15 August | v0.1.2 diff --git a/README.md b/README.md index 3c63c94..7607402 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ import "github.com/kataras/golog" func main() { golog.SetLevel("debug") - golog.Handle(golog.JSON(" ")) + golog.SetFormat("json", " ") // < -- // main.go#29 golog.Debugf("This is a %s with data (debug prints the stacktrace too)", "message", golog.Fields{ @@ -232,7 +232,29 @@ func main() { } ``` -### Custom Format +### Register custom Formatter + +```go +golog.RegisterFormatter(new(myFormatter)) +golog.SetFormat("myformat", options...) +``` + +The `Formatter` interface looks like this: + +```go +// Formatter is responsible to print a log to the logger's writer. +type Formatter interface { + // The name of the formatter. + String() string + // Set any options and return a clone, + // generic. See `Logger.SetFormat`. + Options(opts ...interface{}) Formatter + // Writes the "log" to "dest" logger. + Format(dest io.Writer, log *Log) bool +} +``` + +### Custom Format using `Handler` **Create a JSON handler** diff --git a/_examples/customize-output/main.go b/_examples/customize-output/main.go index 61f0876..315e5e9 100644 --- a/_examples/customize-output/main.go +++ b/_examples/customize-output/main.go @@ -5,7 +5,9 @@ import "github.com/kataras/golog" func main() { golog.SetLevel("debug") - golog.Handle(golog.JSON(" ")) // < -- + golog.SetFormat("json", " ") // < -- + // To register a custom formatter: + // golog.RegisterFormatter(golog.Formatter...) /* Example Output: { "timestamp": 1591423477, diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..7b60233 --- /dev/null +++ b/formatter.go @@ -0,0 +1,80 @@ +package golog + +import ( + "encoding/json" + "io" + "sync" +) + +// Formatter is responsible to print a log to the logger's writer. +type Formatter interface { + // The name of the formatter. + String() string + // Set any options and return a clone, + // generic. See `Logger.SetFormat`. + Options(opts ...interface{}) Formatter + // Writes the "log" to "dest" logger. + Format(dest io.Writer, log *Log) bool +} + +// JSONFormatter is a Formatter type for JSON logs. +type JSONFormatter struct { + Indent string + + // use one encoder per level, do not create new each time. + encoders map[Level]*json.Encoder + mu sync.RWMutex // encoders locker. + encMu sync.Mutex // encode action locker. +} + +// String returns the name of the Formatter. +// In this case it returns "json". +// It's used to map the formatter names with their implementations. +func (f *JSONFormatter) String() string { + return "json" +} + +// Options sets the options for the JSON Formatter (currently only indent). +func (f *JSONFormatter) Options(opts ...interface{}) Formatter { + formatter := &JSONFormatter{ + Indent: " ", + encoders: make(map[Level]*json.Encoder, len(Levels)), + } + + for _, opt := range opts { + if opt == nil { + continue + } + + if indent, ok := opt.(string); ok { + formatter.Indent = indent + break + } + } + + return formatter +} + +// Format prints the logs in JSON format. +// +// Usage: +// logger.SetFormat("json") or +// logger.SetLevelFormat("info", "json") +func (f *JSONFormatter) Format(dest io.Writer, log *Log) bool { + f.mu.RLock() + enc, ok := f.encoders[log.Level] + f.mu.RUnlock() + + if !ok { + enc = json.NewEncoder(dest) + enc.SetIndent("", f.Indent) + f.mu.Lock() + f.encoders[log.Level] = enc + f.mu.Unlock() + } + + f.encMu.Lock() + err := enc.Encode(log) + f.encMu.Unlock() + return err == nil +} diff --git a/golog.go b/golog.go index 23ca9ff..259111c 100644 --- a/golog.go +++ b/golog.go @@ -61,6 +61,21 @@ func SetStacktraceLimit(limit int) *Logger { return Default.SetStacktraceLimit(limit) } +// RegisterFormatter registers a Formatter for this logger. +func RegisterFormatter(f Formatter) *Logger { + return Default.RegisterFormatter(f) +} + +// SetFormat sets a default formatter for all log levels. +func SetFormat(formatter string, opts ...interface{}) *Logger { + return Default.SetFormat(formatter, opts...) +} + +// SetLevelFormat changes the output format for the given "levelName". +func SetLevelFormat(levelName string, formatter string, opts ...interface{}) *Logger { + return Default.SetLevelFormat(levelName, formatter, opts...) +} + // SetLevelOutput sets a destination log output for the specific "levelName". // For multiple writers use the `io.Multiwriter` wrapper. func SetLevelOutput(levelName string, w io.Writer) *Logger { diff --git a/json.go b/json.go deleted file mode 100644 index cb6ee95..0000000 --- a/json.go +++ /dev/null @@ -1,37 +0,0 @@ -package golog - -import ( - "encoding/json" - "sync" -) - -// JSON returns a new JSON handler. -// The logger will print the logs in JSON format. -// -// Usage: -// logger.Handle(JSON(" ")) -func JSON(indent string) Handler { - // use one encoder per level, do not create new each time. - encoders := make(map[Level]*json.Encoder, len(Levels)) - mu := new(sync.RWMutex) // encoders locker. - encMu := new(sync.Mutex) // encode action locker. - - return func(l *Log) bool { - mu.RLock() - enc, ok := encoders[l.Level] - mu.RUnlock() - - if !ok { - enc = json.NewEncoder(l.Logger.getLevelOutput(l.Level)) - enc.SetIndent("", indent) - mu.Lock() - encoders[l.Level] = enc - mu.Unlock() - } - - encMu.Lock() - err := enc.Encode(l) - encMu.Unlock() - return err == nil - } -} diff --git a/logger.go b/logger.go index 3d99250..70cd958 100644 --- a/logger.go +++ b/logger.go @@ -46,6 +46,10 @@ type Logger struct { // The per log level raw writers, optionally. LevelOutput map[Level]io.Writer + formatters map[string]Formatter // available formatters. + formatter Formatter // the current formatter for all logs. + LevelFormatter map[Level]Formatter // per level formatter. + handlers []Handler once sync.Once logs sync.Pool @@ -61,7 +65,11 @@ func New() *Logger { NewLine: true, Printer: pio.NewPrinter("", os.Stdout).EnableDirectOutput().Hijack(logHijacker).SetSync(true), LevelOutput: make(map[Level]io.Writer), - children: newLoggerMap(), + formatters: map[string]Formatter{ // the available builtin formatters. + "json": new(JSONFormatter), + }, + LevelFormatter: make(map[Level]Formatter), + children: newLoggerMap(), } } @@ -113,7 +121,13 @@ var logHijacker = func(ctx *pio.Ctx) { logger.mu.Lock() defer logger.mu.Unlock() - w := logger.getLevelOutput(l.Level) + w := logger.getOutput(l.Level) + if f := logger.getFormatter(); f != nil { + if f.Format(w, l) { + ctx.Store(nil, pio.ErrHandled) + return + } + } if l.Level != DisableLevel { if level, ok := Levels[l.Level]; ok { @@ -211,6 +225,57 @@ func (l *Logger) DisableNewLine() *Logger { return l } +// RegisterFormatter registers a Formatter for this logger. +func (l *Logger) RegisterFormatter(f Formatter) *Logger { + l.mu.Lock() + l.formatters[f.String()] = f + l.mu.Unlock() + return l +} + +// SetFormat sets a formatter for all logger's logs. +func (l *Logger) SetFormat(formatter string, opts ...interface{}) *Logger { + l.mu.RLock() + f, ok := l.formatters[formatter] + l.mu.RUnlock() + + if ok { + l.mu.Lock() + l.formatter = f.Options(opts...) + l.mu.Unlock() + } + + return l +} + +// SetLevelFormat changes the output format for the given "levelName". +func (l *Logger) SetLevelFormat(levelName string, formatter string, opts ...interface{}) *Logger { + l.mu.RLock() + f, ok := l.formatters[formatter] + l.mu.RUnlock() + + if ok { + l.mu.Lock() + l.LevelFormatter[ParseLevel(levelName)] = f.Options(opts...) + l.mu.Unlock() + } + + return l +} + +func (l *Logger) getFormatter() Formatter { + f, ok := l.LevelFormatter[l.Level] + if !ok { + if l.formatter != nil { + f = l.formatter + } else { + f = nil + } + } + + return f +} + // SetLevelOutput sets a destination log output for the specific "levelName". // For multiple writers use the `io.Multiwriter` wrapper. func (l *Logger) SetLevelOutput(levelName string, w io.Writer) *Logger { @@ -225,12 +290,12 @@ func (l *Logger) SetLevelOutput(levelName string, w io.Writer) *Logger { // the logger's default printer. It does NOT return nil. func (l *Logger) GetLevelOutput(levelName string) io.Writer { l.mu.RLock() - w := l.getLevelOutput(ParseLevel(levelName)) + w := l.getOutput(ParseLevel(levelName)) l.mu.RUnlock() return w } -func (l *Logger) getLevelOutput(level Level) io.Writer { +func (l *Logger) getOutput(level Level) io.Writer { w, ok := l.LevelOutput[level] if !ok { w = l.Printer @@ -514,23 +579,34 @@ func (l *Logger) Scan(r io.Reader) (cancel func()) { // Clone returns a copy of this "l" Logger. // This copy is returned as pointer as well. func (l *Logger) Clone() *Logger { - // copy level output map. + // copy level output and format maps. + formats := make(map[string]Formatter, len(l.formatters)) + for k, v := range formats { + formats[k] = v + } + levelFormat := make(map[Level]Formatter, len(l.LevelFormatter)) + for k, v := range l.LevelFormatter { + levelFormat[k] = v + } levelOutput := make(map[Level]io.Writer, len(l.LevelOutput)) for k, v := range l.LevelOutput { levelOutput[k] = v } return &Logger{ - Prefix: l.Prefix, - Level: l.Level, - TimeFormat: l.TimeFormat, - NewLine: l.NewLine, - Printer: l.Printer, - LevelOutput: levelOutput, - handlers: l.handlers, - children: newLoggerMap(), - mu: sync.RWMutex{}, - once: sync.Once{}, + Prefix: l.Prefix, + Level: l.Level, + TimeFormat: l.TimeFormat, + NewLine: l.NewLine, + Printer: l.Printer, + LevelOutput: levelOutput, + formatter: l.formatter, + formatters: formats, + LevelFormatter: levelFormat, + handlers: l.handlers, + children: newLoggerMap(), + mu: sync.RWMutex{}, + once: sync.Once{}, } }