/logze

Structural logging with zerolog efficiency and slog interface

Primary LanguageGoMIT LicenseMIT

logze

Go Version GoDoc Build GoReport

logze is a logging package that wraps the efficient zerolog, providing a user-friendly interface similar to slog. It offers the performance and features of zerolog with an interface that developers familiar with slog can easily adapt to.

Table of Contents

Why you should try

If you want to log an error in zerolog you will write something like this:

log.Error().Err(err).Str("address", "127.0.0.1").Int("retry", n).Msg("cannot start server")

This is a quite long piece of code and you should remember the names of these methods. Let's look at a logze example:

logze.Err(err, "cannot start server", "address", "127.0.0.1", "retry", n)

Firstly, it is shorter. Secondly, you call only one method, which names Err, like an err that you want to log.

In other hand, you may say, that it is better to use slog with the similar interface instead of logze. But logze also provides a zerolog efficency — it is 3 times faster that slog and only 15% slower that zerolog due to using Fields() instead of separate method for each type.

Installation

To install logze, use:

go get -u github.com/maxbolgarin/logze/v2

Usage

Creating a Logger

To start using logze in your project, you can initialize a logger as follows:

package main

import (
	"github.com/maxbolgarin/logze/v2"
)

func main() {
	logger := logze.New(logze.C().WithConsoleJSON(), "application", "logze-example")
	
	logger.Info("Starting application", "version", "1.0.0")
}

// Output: {"level":"info","message":"Starting application","version":"1.0.0","application":"logze-example","time":"2023-08-24T15:30:00Z"}

Logging Messages

Once you have a logger, you can log messages at various levels:

logger.Trace("Trace application", "version", "1.0.0")
logger.Debug("Debugging application start", "version", "1.0.0") 
logger.Info("Application started successfully", "number", 123) 
logger.Warn("Low memory warning", "data", map[string]int{"key1": 1, "key2": 2}) 
logger.Error("Disk space low", "error", errors.New("disk space error")) 
logger.Err(errors.New("disk space error"), "disk space low") 

/* Example Output:
{"level":"trace","caller":"/Users/alex/code/logze/logze_test.go:210","time":"2024-12-12T17:24:24+03:00","message":"Trace application","version":"1.0.0"}
{"level":"debug","message":"Debugging application start","version":"1.0.0","time":"2023-08-24T15:30:00Z"}
{"level":"info","message":"Application started successfully","number":123,"time":"2023-08-24T15:30:00Z"}
{"level":"warning","message":"Low memory warning","data":{"key1":1,"key2":2},"time":"2023-08-24T15:30:00Z"}
{"level":"error","message":"Disk space low","error":"disk space error","time":"2023-08-24T15:30:00Z"}
{"level":"error","message":"disk space low","error":"disk space error","time":"2023-08-24T15:30:00Z"}
*/

Logging Messages with formatting

You can also log messages with formatting and fields in the structured way: you add formatting args and then key-value pairs. For example:

logger.Debugf("Debugging application start at %s", time.Now(), "version", "1.0.0")
logger.Infof("Application %s started successfully", "name", "number", 123)
logger.Warnf("Low memory warning %d times", times)

// {"level":"debug","message":"Debugging application start at 2023-08-24T15:30:00Z","version":"1.0.0","time":"2023-08-24T15:30:00Z"}
// {"level":"info","message":"Application name started successfully","number":123,"time":"2023-08-24T15:30:00Z"}
// {"level":"warning","message":"Low memory warning 3 times","time":"2023-08-24T15:30:00Z"}

Global logger usage

You can use the global logger from logze package instead of creating a new one:

// Init global logger with trace level and one field pair (optional to provide options and fields)
logze.Init(logze.NewConfig().WithConsole().WithLevel(logze.TraceLevel), "foo", "bar")

// Trace log will print Caller
logze.Trace("trace message")

// Example of logging an error
logze.Err(errm.New("some_error"), "message")


// Here is the result	
// 10:32:57 TRC test/main.go:16 > trace message foo=bar
// 10:32:57 ERR message error=some_error foo=bar

Configuration Options

You can configure the logger with various options:

  • Log Level: Set the log level (trace, debug, info, warn, error, fatal).
  • Many Output Writers: Direct logs to console, files or network writers, you can provide as many io.Writer as you want.
  • Ignore Messages: Ignore specific log messages using WithToIgnore, that will check using strings.Contains on log message.
  • Error Counter: Add error counters using WithErrorCounter or WithSimpleErrorCounter; it may be useful for metrics to count errors.
  • Stack Trace: Enable/disable stack trace of errors; you can use errm to get stack trace out of the box.
  • Diode Buffering: Enable/disable and configure diode buffering.

Example:

config := logze.NewConfig(w1, w2).
    WithConsole().
	WithLevel(logze.DebugLevel).
	WithToIgnore("ignore me").
	WithSimpleErrorCounter().
    WithStackTrace().
	WithDiodeSize(10000)

logger := logze.New(config)

Pros and Cons

Pros

  • High Performance: 3 times faster than slog, leveraging zerolog efficient engine.
  • Structured Logging: Native support for key-value pairs with any type of value.
  • Easy Transition: Compatible interface with slog.
  • Easy Configuration: Simplified configuration and initialization process.
  • Diode Buffering: Supports non-blocking IO operations for high performance.

Cons

  • Slight Overhead: Approximately 15% slower than raw zerolog due to the usage of fields instead of typed methods.
  • Complexity: Advanced features such as diode might be complex for new users.

Using Diode

By default, logze uses diode buffering to prevent blocking on log IO operations. This ensures high throughput but requires careful shutdown handling to prevent log loss. It's crucial to note that if your application shuts down immediately after logging, some log messages might be lost. This can be mitigated waiting for flushing logs or disabling the diode feature. Diode also can drop messages if too many logs are generated in a short span.

To disable diode buffering:

config := logze.NewConfig().WithNoDiode()
logger := logze.New(config)

Benchmarks

Thoughts from benchmarks:

  • logze is about 3 times faster than slog and for 15% slower than zerolog
  • format methods like logze.Infof or Msgf doesn't add big overhead (only 30% slower and +1 alloc)
  • stack trace is very slow in logze and zerolog
  • console writer is very slow in logze and zerolog and should be used only in development (that the case when slog wins over logze - if you want to use text writer in production)
  • logze.Err is slightly faster that logze.Error

Here is result of go test -bench=. -benchmem -benchtime=2s:

goos: darwin
goarch: arm64
cpu: Apple M1 Pro

Logging Info and two fields:

BenchmarkZerologInfo-8                  14365629               151.3 ns/op             0 B/op          0 allocs/op
BenchmarkLogzeInfo-8                    13851320               171.5 ns/op             0 B/op          0 allocs/op
BenchmarkSLogInfo-8                      4491897               533.2 ns/op             0 B/op          0 allocs/op

Logging Infof (formatted) and two fields:

BenchmarkZerologInfoFormat-8            11758495               207.0 ns/op            24 B/op          1 allocs/op
BenchmarkLogzeInfoFormat-8              10047972               239.8 ns/op            24 B/op          1 allocs/op
BenchmarkSLogInfoFormat-8                4026775               601.0 ns/op            24 B/op          1 allocs/op

Logging Error with error and two fields:

BenchmarkZerologError-8                 13900646               173.6 ns/op             0 B/op          0 allocs/op
BenchmarkLogzeError-8                   10827030               221.7 ns/op             0 B/op          0 allocs/op
BenchmarkSLogError-8                     3757587               635.7 ns/op             0 B/op          0 allocs/op

Logging Error with error, stack trace and two fields:

BenchmarkZerologErrorWithStack-8          545072               4379 ns/op            3298 B/op         73 allocs/op
BenchmarkLogzeErrorWithStack-8            291956               8065 ns/op            5460 B/op         121 allocs/op
BenchmarkSLogErrorWithStack-8             612562               3922 ns/op            1617 B/op         3 allocs/op

Logging Info and two fields using text handler for console / development:

BenchmarkZerologInfoConsole-8             682558               3306 ns/op            1922 B/op         51 allocs/op
BenchmarkLogzeInfoConsole-8               704247               3366 ns/op            1922 B/op         51 allocs/op
BenchmarkSLogInfoConsole-8               4058850               588.1 ns/op             0 B/op          0 allocs/op

Additional logze features

BenchmarkLogzeErr-8                     11857878               200.9 ns/op             0 B/op          0 allocs/op
BenchmarkLogzeToIgnore5-8               10040202               238.5 ns/op             0 B/op          0 allocs/op

Contributing

Contributions are welcome! Please open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE file for details.