Skip to content

Commit

Permalink
Add new ***Fields methods
Browse files Browse the repository at this point in the history
Right now developer can use fluent-interface API to log message with fields, for example:

```go
log.With("key1","value").
    With("key2","value").
    Info(ctx, "message")
```

For some such design is not intuitive. They prefer to pass fields directly as an Info method parameter. Preferably a map. For example:

```go
log.InfoFields(ctx, "message", logger.Fields{
    "key1": "value",
    "key2": "value",
})
```

Also, the performance of fluent-interface design is not very high. Each With method call creates a new instance of logger with additional field which costs 1 allocation (for normal logger) and 2 allocations (for global one). Most of the time, such logger will be discarded soon (in the very same method), so allocation could be avoided.

The proposed new logger methods can simplify the use of logger and improve the performance.

Here are the changes:

* new logger.Fields type which is a just a map[string]interface{}
* both loggers (logger.Logger and logger.Global) have new methods for directly passing fields:
  * DebugFields(ctx context.Context, msg string, fields Fields)
  * InfoFields(ctx context.Context, msg string, fields Fields)
  * WarnFields(ctx context.Context, msg string, fields Fields)
  * ErrorFields(ctx context.Context, msg string, fields Fields)
* both loggers have new methods for passing an error too:
  * ErrorCause(ctx context.Context, msg string, cause error)
  * ErrorCauseFields(ctx context.Context, msg string, cause error, fields Fields)
* both loggers have new methods for creating a child logger with multiple fields at once:
  * WithFields(Fields)
* logger.Entry has a new method WithFields(Fields)

**All the changes are backwards compatible.**
  • Loading branch information
elgopher committed Feb 10, 2022
1 parent 9981ac9 commit 4180f24
Show file tree
Hide file tree
Showing 19 changed files with 675 additions and 155 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ func SetLoggerAdapter(adapter logger.Adapter) {
func Function(ctx context.Context) {
log.Debug(ctx, "Debug message")

log.With("field_name", "value").
Info(ctx, "Message with field")
log.InfoFields(ctx, "Message with field", logger.Fields{
"field_name": "value",
"other_name": "value",
})

log.WithError(errors.New("some")).
Error(ctx, "Message with error")
log.ErrorCause(ctx, "Message with error", errors.New("some"))
}
```

Expand Down Expand Up @@ -207,5 +208,5 @@ Unfortunately such interface is much harder to implement, than interface with a

* even though your package will be independent of any specific logging implementation, you still have to import
`github.com/elgopher/yala/logger`. This package is relatively small though, compared to real logging libraries
(about ~200 lines of production code) and **it does not import any external libraries**.
(about ~250 lines of production code) and **it does not import any external libraries**.
* yala is not optimized for **extreme** high performance, because this would hurt the developer experience and readability of the created code. Any intermediary API ads overhead - global synchronized variables, wrapper code and even polymorphism slow down the execution a bit. The overhead varies, but it is usually a matter of tens of nanoseconds per call.
14 changes: 8 additions & 6 deletions adapter/console/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ func main() {

log.Debug(ctx, "Hello fmt")

log.With("field_name", "field_value").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some value").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some value",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
log.ErrorCause(ctx, "Some error", ErrSome)
}
14 changes: 8 additions & 6 deletions adapter/glogadapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ func main() {

log.Debug(ctx, "Hello glog ") // Debug will be logged as Info

log.With("field_name", "field_value").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Error occurred")
log.ErrorCause(ctx, "Error occurred", ErrSome)
}
17 changes: 16 additions & 1 deletion adapter/internal/benchmark/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

// Adapter runs benchmarks on any implementation of logger.Adapter.
func Adapter(b *testing.B, adapter logger.Adapter) {
func Adapter(b *testing.B, adapter logger.Adapter) { // nolint:funlen
b.Helper()

ctx := context.Background()
Expand All @@ -29,6 +29,21 @@ func Adapter(b *testing.B, adapter logger.Adapter) {
}
})

b.Run("global logger info with two fields", func(b *testing.B) {
var global logger.Global
global.SetAdapter(adapter)

b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
global.InfoFields(ctx, "msg", logger.Fields{
"field1": "value",
"field2": "value",
})
}
})

b.Run("normal logger info", func(b *testing.B) {
log := logger.WithAdapter(adapter)

Expand Down
14 changes: 8 additions & 6 deletions adapter/log15adapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ func main() {

log.Debug(ctx, "Hello log15")

log.With("field_name", "field_value").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
log.ErrorCause(ctx, "Some error", ErrSome)
}
19 changes: 10 additions & 9 deletions adapter/logadapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ func main() {
// log using standard log package
standardLog := log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
adapter := logadapter.Adapter(standardLog)
log := logger.WithAdapter(adapter)
yalaLogger := logger.WithAdapter(adapter)

log.Debug(ctx, "Hello standard log")
yalaLogger.Debug(ctx, "Hello standard log")

log.With("f1", "v1").
With("f2", "f2").
Info(ctx, "Some info")
yalaLogger.InfoFields(ctx, "Some info", logger.Fields{
"f1": "v1",
"f2": "v2",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
yalaLogger.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
yalaLogger.ErrorCause(ctx, "Some error", ErrSome)
}
17 changes: 9 additions & 8 deletions adapter/logrusadapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ func main() {
// Create yala logger
log := logger.WithAdapter(adapter)

log.Debug(ctx, "Hello logrus ")
log.Debug(ctx, "Hello logrus")

log.With("field_name", "field_value").
With("another", "ccc").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
log.ErrorCause(ctx, "Some error", ErrSome)
}

func newLogrusLogger() *logrus.Logger {
Expand Down
14 changes: 8 additions & 6 deletions adapter/zapadapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ func main() {

log.Debug(ctx, "Hello zap")

log.With("field_name", "field_value").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
log.ErrorCause(ctx, "Some error", ErrSome)
}

func newZapLogger() *zap.Logger {
Expand Down
14 changes: 8 additions & 6 deletions adapter/zerologadapter/_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ func main() {

log.Debug(ctx, "Hello zerolog")

log.With("field_name", "field_value").
Info(ctx, "Some info")
log.InfoFields(ctx, "Some info", logger.Fields{
"field_name": "field_value",
"other_name": "field_value",
})

log.With("parameter", "some").
Warn(ctx, "Deprecated configuration parameter. It will be removed.")
log.WarnFields(ctx, "Deprecated configuration parameter. It will be removed.", logger.Fields{
"parameter": "some",
})

log.WithError(ErrSome).
Error(ctx, "Some error")
log.ErrorCause(ctx, "Some error", ErrSome)
}
6 changes: 4 additions & 2 deletions logger/_examples/caller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ func (a ReportCallerAdapter) Log(ctx context.Context, entry logger.Entry) {
entry.SkippedCallerFrames++ // each middleware adapter must additionally skip one frame (at least)

if _, file, line, ok := runtime.Caller(entry.SkippedCallerFrames); ok {
entry = entry.With(logger.Field{Key: "file", Value: file})
entry = entry.With(logger.Field{Key: "line", Value: line})
entry = entry.WithFields(logger.Fields{
"file": file,
"line": line,
})
}

a.NextAdapter.Log(ctx, entry)
Expand Down
5 changes: 3 additions & 2 deletions logger/_examples/rename/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ func main() {

log := logger.WithAdapter(adapter)

log.With("this", "value").
Info(ctx, "this field will be replaced with that")
log.InfoFields(ctx, "this field will be replaced with that", logger.Fields{
"this": "value",
})
}

// RenameFieldsAdapter is a middleware (decorator) renaming all fields equal to From into To.
Expand Down
12 changes: 8 additions & 4 deletions logger/_examples/reuse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ func main() {
log := logger.WithAdapter(console.StdoutAdapter())

// requestLogger will log all messages with at least two fields: request_id and user
requestLogger := log.With("request_id", "123").With("user", "elgopher")
requestLogger := log.WithFields(logger.Fields{
"request_id": "123",
"user": "elgopher",
})

requestLogger.Debug(ctx, "request started")

requestLogger.
With("rows_updated", 3).
With("table", "gophers").
Debug(ctx, "sql update executed")
DebugFields(ctx, "sql update executed", logger.Fields{
"rows_updated": 3,
"table": "gophers",
})

requestLogger.Debug(ctx, "request finished")
}
14 changes: 5 additions & 9 deletions logger/_examples/tags/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ func main() {
// log.Info() -> AddFieldFromContextAdapter -> console adapter
log.Info(ctx, "tagged message") // INFO tagged message tag=value

log.With("k", "v").
Info(ctx, "tagged message") // INFO tagged message k=v tag=value
log.InfoFields(ctx, "tagged message", logger.Fields{"k": "v"}) // INFO tagged message k=v tag=value
}

// AddFieldFromContextAdapter is a middleware (decorator) which adds
Expand All @@ -36,13 +35,10 @@ type AddFieldFromContextAdapter struct {
}

func (a AddFieldFromContextAdapter) Log(ctx context.Context, entry logger.Entry) {
// entry.With creates an entry and adds a new field to it
newEntry := entry.With(
logger.Field{
Key: tag,
Value: ctx.Value(tag),
},
)
// entry.WithFields creates a copy of the entry with additional fields
newEntry := entry.WithFields(logger.Fields{
tag: ctx.Value(tag),
})
newEntry.SkippedCallerFrames++ // each middleware adapter must additionally skip one frame
a.NextAdapter.Log(ctx, newEntry)
}
21 changes: 21 additions & 0 deletions logger/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ func (e Entry) With(field Field) Entry {
return e
}

type Fields map[string]interface{}

// WithFields creates a new entry with additional fields. Fields will be appended, not replaced.
func (e Entry) WithFields(fields Fields) Entry {
if len(fields) == 0 {
return e
}

fieldsLength := len(e.Fields)
slice := make([]Field, len(e.Fields)+len(fields))
copy(slice, e.Fields)
e.Fields = slice

for k, v := range fields {
e.Fields[fieldsLength] = Field{k, v}
fieldsLength++
}

return e
}

// Level is a severity level of message. Use Level.MoreSevereThan to compare two levels.
type Level int8

Expand Down
Loading

0 comments on commit 4180f24

Please sign in to comment.