Skip to content

Commit

Permalink
introduce the Formatter interface (one level higher than the Handler)
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Sep 6, 2020
1 parent fe0f1a7 commit e07ba15
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 56 deletions.
9 changes: 8 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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**

Expand Down
4 changes: 3 additions & 1 deletion _examples/customize-output/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions formatter.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions golog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 0 additions & 37 deletions json.go

This file was deleted.

106 changes: 91 additions & 15 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
}
}

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

Expand Down

0 comments on commit e07ba15

Please sign in to comment.