diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f85a737 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: test test-coverage test-benchmark encoders-benchmark loggers-comparison-benchmark + +ARCH=amd64 + +# Run all tests with coverage profile +test: + CGO_ENABLED=1 GOARCH=${ARCH} go test -cover ./... -race + +# Run all tests and Show HTML coverage report in the browser +test-coverage: + CGO_ENABLED=1 GOARCH=${ARCH} go test -cover -coverprofile=coverage.out ./... -race && go tool cover -html=coverage.out + +# Run encoders benchmark tests +encoders-benchmark: + CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/encoders_benchmark_test.go ./test/functions.go -bench=. -benchmem -benchtime=5s -cpu=8 + +# Run commercial loggers benchmark comparison tests +loggers-comparison-benchmark: + CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/loggers_comparison_test.go ./test/functions.go -bench=. -benchmem -benchtime=5s -cpu=8 \ No newline at end of file diff --git a/README.md b/README.md index eff356f..728798e 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,30 @@ # Tiny Logger -A fast, lightweight, zero-dependency logging solution for Go applications that prioritizes performance and -simplicity. -Compatible with Go version 1.18.x and above +A fast, lightweight, zero-dependency logging solution for Go applications that prioritizes performance and simplicity. -## ✅ Key Features +This library is extremely optimized to log loosely-typed data (interfaces), so you won't have to specify concrete types before logging. -- **Lightweight**: No external dependencies mean faster builds and smaller binaries
- The dependencies that you see in the `go.mod` file are not included in the final binary since they are only used in `_test` files. -- **Simplicity**: Clean API design with a minimal learning curve. You'll set it up in seconds. +I know that higher raw speed can be reached using other Go logging solutions, but when I created tiny-logger, I wanted to build something as fast as possible without compromising on simplicity of use. +There are many projects that can benefit from having a logging library that is compact, fast, easy to use, and simple to modify. +Since the codebase is so small, it won't take long for you to understand it and modify it at will. + +The project is compatible with **Go version 1.18.x** and above. + +## Key Features + +- **Lightweight**: The library has no dependencies, the code you see is all that runs. + NOTE: The only dependencies you'll see in the `go.mod` file are not included in the final binary since they are only used in `_test` files. +- **Simplicity**: I designed the API to have a minimal learning curve. You'll set it up in seconds. - **Performance**: The library is benchmarked to be very fast. It implements custom JSON and YAML marshaling specifically optimized for logging - - Up to 1.4x faster JSON marshaling than `encoding/json` - - Up to 5x faster YAML marshaling than `gopkg.in/yaml.v3` -- **Color Support**: Built-in ANSI color support for terminal output -- **Thread-Safe**: Concurrent-safe logging with atomic operations -- **Time-Optimized**: Efficient date/time print built-int logic with minimal allocations -- **Reliability**: Thoroughly tested with high test coverage -- **Maintainability**: A small, focused codebase makes it easy to understand and modify at will + - Up to 1.4x faster JSON marshaling than `encoding/json` + - Up to 5x faster YAML marshaling than `gopkg.in/yaml.v3` +- **Color Support**: Built-in ANSI color support for terminal output. +- **Thread-Safe**: Concurrent-safe logging with atomic operations. +- **Time-Optimized**: Efficient date/time formatting with minimal allocations. +- **Memory-Efficient**: Heap allocations and log sizes are kept to a minimum to avoid triggering the garbage collector. -## 🎯 Use Examples +## Use Examples ````go /******************** Basic Logging methods usage ********************/ @@ -64,44 +69,64 @@ logger.SetDateTimeFormat(shared.UnixTimestamp) logger.Debug("This is my Debug log", "Test arg") // stdout: 1690982143.000000 This is my Debug log Test arg ```` -## 📊 Benchmark Results +## Benchmarks + +1. Benchmarks of the **loggers-comparison-benchmark** in [Makefile](./Makefile) + + **NOTE:** These benchmarks intentionally log loosely-typed data across all four compared libraries. + As mentioned above, libraries like **zerolog** can achieve higher performance when logging strictly-defined data types. + However, since high-speed typed logging was not the primary goal of **tiny-logger**, I wanted to evaluate how it performs against industry-standard libraries when handling arbitrary data types. + + + - **OS:** Linux + - **Arch:** AMD64 + - **CPU:** 12th Gen Intel(R) Core(TM) i9-12900K -This is the result of running the `./test/benchmark_test.go` benchmark on my machine, (ns/op)times do not include the -terminal graphical visualization time. + | Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op | + | :--- | :--- | :--- | :--- | :--- | + | **TinyLogger** | 17,625,723 | **339.9 ns** | **88 B** | **2** | + | Zerolog | 12,983,034 | 460.2 ns | 232 B | 5 | + | Zap | 10,391,967 | 578.3 ns | 136 B | 2 | + | Logrus | 3,607,248 | 1692 ns | 1241 B | 21 | -| Encoder | Configuration | ns/op | B/op | allocs/op | -|---------------------|--------------------|-------|------|-----------| -| **Default Encoder** | All Properties OFF | 490.3 | 80 | 1 | -| | All Properties ON | 511.2 | 104 | 1 | -| **JSON Encoder** | All Properties OFF | 513.3 | 80 | 1 | -| | All Properties ON | 536.5 | 104 | 1 | -| **YAML Encoder** | All Properties OFF | 535.3 | 80 | 1 | -| | All Properties ON | 557.1 | 104 | 1 | + - **OS:** Darwin (macOS) + - **Arch:** AMD64 + - **CPU:** VirtualApple @ 2.50GHz + | Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op | + | :--- | :--- | :--- | :--- | :--- | + | **TinyLogger** | 6,091,185 | **972.9 ns** | **88 B** | **2** | + | Zerolog | 4,922,115 | 1220 ns | 232 B | 5 | + | Zap | 3,938,301 | 1517 ns | 136 B | 2 | + | Logrus | 1,814,809 | 3291 ns | 1241 B | 21 | -## 🤝 Contributing +2. Benchmarks of the **encoders-benchmark** command contained in the [Makefile](./Makefile) -Contributions are welcome, Here's how you can help: + - **OS:** Linux + - **Arch:** AMD64 + - **CPU:** 12th Gen Intel(R) Core(TM) i9-12900K -1. Fork the repository -2. Clone your fork: -3. Create a new branch: + | Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op | + | :--- | :--- | :--- | :--- | :--- | + | DefaultEncoder DisabledProperties | 18336217 | 298.7 ns | 88 B | 2 | + | DefaultEncoder EnabledProperties | 18336217 | 334.3 ns | 88 B | 2 | + | JsonEncoder DisabledProperties | 17974824 | 316.0 ns | 88 B | 2 | + | JsonEncoder EnabledProperties | 17488896 | 344.2 ns | 88 B | 2 | + | YamlEncoder DisabledProperties | 17625220 | 342.8 ns | 88 B | 2 | + | YamlEncoder EnabledProperties | 16005187 | 373.3 ns | 88 B | 2 | -```bash - git checkout -b feat/your-feature-name - ``` +## Contributing -- **Code Style** - - Follow standard Go formatting (`go fmt`) - - Use meaningful variable names - - Add comments for non-obvious code sections - - Write tests for new functionality +Contributions to this project are very welcome, here's how you can do it: -- **Testing** - - Run tests: `make test` - - Run benchmarks: `make test-benchmark` - - Ensure test coverage remains high; it can be checked using `make test-coverage` + 1. Fork the repository + 2. Clone your fork + 3. Create a new branch + ```bash git checkout -b your-feature-name``` + 4. Local Tests + Take a look at the [Makefile](./Makefile). + You can use the commands provided to run `test`, check the `test-coverage` and monitor the library's `benchmarks`. -## 📝 License +## License -MIT License—see [LICENSE](https://mit-license.org/) file for details +MIT License—see [LICENSE](https://mit-license.org/) file for details diff --git a/go.mod b/go.mod index 4ea58d4..4bcfbdc 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,20 @@ module github.com/pho3b/tiny-logger -go 1.23.2 +go 1.24.0 require ( + github.com/rs/zerolog v1.34.0 + github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 + go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index fe3d5ca..9162290 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,35 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/services/constants.go b/internal/services/constants.go deleted file mode 100644 index 1c5da00..0000000 --- a/internal/services/constants.go +++ /dev/null @@ -1,5 +0,0 @@ -package services - -const jsonCharOverhead = 80 -const yamlCharOverhead = 70 -const averageExtraLen = 30 diff --git a/internal/services/json_marshaler.go b/internal/services/json_marshaler.go index b0646a2..6304ff1 100644 --- a/internal/services/json_marshaler.go +++ b/internal/services/json_marshaler.go @@ -6,6 +6,11 @@ import ( "strconv" ) +const ( + jsonCharOverhead = 80 + averageExtraLen = 30 +) + // JsonLogEntry represents a structured log entry that can be marshaled to JSON format. // All fields except Message are optional and will be omitted if empty. type JsonLogEntry struct { diff --git a/internal/services/json_mashaler_test.go b/internal/services/json_mashaler_test.go index ee66347..b3d6b8b 100644 --- a/internal/services/json_mashaler_test.go +++ b/internal/services/json_mashaler_test.go @@ -143,7 +143,6 @@ func TestJsonMarshaler_Marshal_OnlyTime(t *testing.T) { m.MarshalInto(buf, entry) got := buf.String() - // Expecting "ts" key instead of "datetime" want := `{"level":"info","time":"16:00","msg":"only time"}` if got != want { t.Errorf("Marshal() = %q, want %q", got, want) diff --git a/internal/services/yaml_marshaler.go b/internal/services/yaml_marshaler.go index 8cc8ced..59bacbf 100644 --- a/internal/services/yaml_marshaler.go +++ b/internal/services/yaml_marshaler.go @@ -6,6 +6,8 @@ import ( "strconv" ) +const yamlCharOverhead = 70 + // YamlLogEntry represents a structured log entry that can be marshaled to YAML format. // All fields except Message are optional and will be omitted if empty. type YamlLogEntry struct { diff --git a/logs/encoders/base.go b/logs/encoders/base.go index d673227..e2e2b14 100644 --- a/logs/encoders/base.go +++ b/logs/encoders/base.go @@ -19,7 +19,9 @@ type baseEncoder struct { bufferSyncPool sync.Pool } -// castAndConcatenateInto writes all the given arguments cast to string and concatenated by a white space into the given buffer. +// castAndConcatenateInto writes all the given arguments cast to string and concatenated by a white space +// into the given buffer. +// The function uses the slower fmt.Sprint only for unknown types func (b *baseEncoder) castAndConcatenateInto(buf *bytes.Buffer, args ...any) { argsLen := len(args) buf.Grow(averageWordLen * argsLen) @@ -32,48 +34,92 @@ func (b *baseEncoder) castAndConcatenateInto(buf *bytes.Buffer, args ...any) { switch v := arg.(type) { case string: buf.WriteString(v) - case rune: - buf.WriteRune(v) + case []byte: + buf.Write(v) case int: buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10)) + case int8: + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10)) + case int16: + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10)) + case int32: + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10)) case int64: buf.Write(strconv.AppendInt(buf.AvailableBuffer(), v, 10)) + case uint: + buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10)) + case uint8: + buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10)) + case uint16: + buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10)) + case uint32: + buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10)) + case uint64: + buf.Write(strconv.AppendUint(buf.AvailableBuffer(), v, 10)) + case float32: + buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), float64(v), 'f', -1, 32)) case float64: buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), v, 'f', -1, 64)) case bool: - buf.Write(strconv.AppendBool(buf.AvailableBuffer(), v)) + if v { + buf.WriteString("true") + break + } + + buf.WriteString("false") case fmt.Stringer: buf.WriteString(v.String()) case error: buf.WriteString(v.Error()) default: - // Using the slower fmt.Sprint only for unknown types buf.WriteString(fmt.Sprint(v)) } } } // castToString is a fast casting method that returns the given argument as a string. +// It uses the slow fmt.Sprint only for unknown types func (b *baseEncoder) castToString(arg any) string { switch v := arg.(type) { case string: return v - case rune: + case []byte: return string(v) case int: return strconv.Itoa(v) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) case int64: return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: return strconv.FormatFloat(v, 'f', -1, 64) case bool: - return strconv.FormatBool(v) + if v { + return "true" + } + + return "false" case fmt.Stringer: return v.String() case error: return v.Error() default: - // Using the slower fmt.Sprint only for unknown types return fmt.Sprint(v) } } diff --git a/logs/encoders/base_test.go b/logs/encoders/base_test.go index 6925561..7efae09 100644 --- a/logs/encoders/base_test.go +++ b/logs/encoders/base_test.go @@ -40,6 +40,8 @@ func TestBuildMsg(t *testing.T) { result = encoder.castToString(int64(43)) assert.Equal(t, "43", result) result = encoder.castToString(int32(32234)) + assert.Equal(t, "32234", result) + result = encoder.castToString("緪") assert.Equal(t, "緪", result) result = encoder.castToString(int8(3)) assert.Equal(t, "3", result) @@ -56,7 +58,7 @@ func TestBuildMsgWithCastAndConcatenateInto(t *testing.T) { buf := &bytes.Buffer{} // Test with multiple arguments - encoder.castAndConcatenateInto(buf, "This", "is", 'a', "test") + encoder.castAndConcatenateInto(buf, "This", "is", "a", "test") assert.Equal(t, "This is a test", buf.String()) buf.Reset() @@ -67,7 +69,7 @@ func TestBuildMsgWithCastAndConcatenateInto(t *testing.T) { // Test with various argument types encoder.castAndConcatenateInto(buf, "str", '\n', 2, 2.3, true, nil) - assert.Equal(t, "str \n 2 2.3 true ", buf.String()) + assert.Equal(t, "str 10 2 2.3 true ", buf.String()) buf.Reset() // Test with no arguments @@ -81,7 +83,7 @@ func TestBuildMsgWithCastAndConcatenateInto(t *testing.T) { buf.Reset() // Test with rune and int64 types and struct - encoder.castAndConcatenateInto(buf, 'A', int64(43), errors.New("my error")) + encoder.castAndConcatenateInto(buf, "A", int64(43), errors.New("my error")) assert.Equal(t, "A 43 my error", buf.String()) buf.Reset() } diff --git a/logs/logger_test.go b/logs/logger_test.go index 6a95677..ce6618e 100644 --- a/logs/logger_test.go +++ b/logs/logger_test.go @@ -473,6 +473,53 @@ func TestLogger_LogsRedirectedToFile(t *testing.T) { assert.Contains(t, contentStr, "error message") } +func TestLogger_NestedStructParameterCorrectLogging(t *testing.T) { + type Address struct { + Street string + City string + Zip int + } + + type Contact struct { + Email string + Phone string + ad Address + } + + type User struct { + ID int + Name string + Address Address // Named field + Contact // Embedded (Promoted) field + } + + user := User{ + ID: 1, + Name: "Alice", + Address: Address{ + Street: "123 Go Lane", + City: "Tech City", + Zip: 90210, + }, + Contact: Contact{ + Email: "alice@example.com", + Phone: "555-0199", + ad: Address{ + Street: "123 Go Lane", + City: "Tech City", + Zip: 90210, + }, + }, + } + + logger := NewLogger().SetEncoder(shared.JsonEncoderType) + outMsg := test.CaptureOutput(func() { logger.Debug(user) }) + assert.Contains(t, + outMsg, + "{1 Alice {123 Go Lane Tech City 90210} {alice@example.com 555-0199 {123 Go Lane Tech City 90210}}}", + ) +} + func createMockOutFile(fileName string) *os.File { file, err := os.OpenFile(fmt.Sprintf("./%s", fileName), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { diff --git a/makefile b/makefile deleted file mode 100644 index 854cc38..0000000 --- a/makefile +++ /dev/null @@ -1,15 +0,0 @@ -.PHONY: test test-coverage test-benchmark - -ARCH=amd64 - -# Run all tests with coverage profile -test: - CGO_ENABLED=1 GOARCH=${ARCH} go test -cover ./... -race - -# Run all tests and Show HTML coverage report in the browser -test-coverage: - CGO_ENABLED=1 GOARCH=${ARCH} go test -cover -coverprofile=coverage.out ./... -race && go tool cover -html=coverage.out - -# Run benchmark tests -test-benchmark: - CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/benchmark_test.go -bench=. -benchmem -benchtime=8s -cpu=8 | grep /op diff --git a/test/benchmark_test.go b/test/encoders_benchmark_test.go similarity index 71% rename from test/benchmark_test.go rename to test/encoders_benchmark_test.go index 6ce8a30..da07d2e 100644 --- a/test/benchmark_test.go +++ b/test/encoders_benchmark_test.go @@ -1,7 +1,6 @@ package test import ( - "fmt" "testing" "github.com/pho3b/tiny-logger/logs" @@ -12,13 +11,11 @@ func BenchmarkDefaultEncoderAllPropertiesDisabled(b *testing.B) { b.ReportAllocs() logger := logs.NewLogger(). - SetEncoder(shared.DefaultEncoderType).ShowLogLevel(false) + SetEncoder(shared.DefaultEncoderType).ShowLogLevel(false).SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("DEFAULT encoder", "all-properties-enabled", false, "id", 2) + logger.Debug("DEFAULT encoder", "all-properties-enabled", false, "id", i) } - - fmt.Print("Default_Encoder_All_Properties_Disabled:") } func BenchmarkDefaultEncoderAllPropertiesEnabled(b *testing.B) { @@ -28,26 +25,23 @@ func BenchmarkDefaultEncoderAllPropertiesEnabled(b *testing.B) { SetEncoder(shared.DefaultEncoderType). ShowLogLevel(true). AddDateTime(true). - EnableColors(true) + EnableColors(true). + SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("DEFAULT encoder", "all-properties-enabled", true, "id", 2) + logger.Debug("DEFAULT encoder", "all-properties-enabled", true, "id", i) } - - fmt.Print("Default_Encoder_All_Properties_Enabled: ") } func BenchmarkJsonEncoderAllPropertiesDisabled(b *testing.B) { b.ReportAllocs() logger := logs.NewLogger(). - SetEncoder(shared.JsonEncoderType).ShowLogLevel(false) + SetEncoder(shared.JsonEncoderType).ShowLogLevel(false).SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("JSON encoder", "all-properties-enabled", false, "id", 2) + logger.Debug("JSON encoder", "all-properties-enabled", false, "id", i) } - - fmt.Print("Json_Encoder_All_Properties_Disabled: ") } func BenchmarkJsonEncoderAllPropertiesEnabled(b *testing.B) { @@ -56,26 +50,23 @@ func BenchmarkJsonEncoderAllPropertiesEnabled(b *testing.B) { logger := logs.NewLogger(). SetEncoder(shared.JsonEncoderType). ShowLogLevel(true). - AddDateTime(true) + AddDateTime(true). + SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("JSON encoder", "all-properties-enabled", true, "id", 2) + logger.Debug("JSON encoder", "all-properties-enabled", true, "id", i) } - - fmt.Print("Json_Encoder_All_Properties_Enabled: ") } func BenchmarkYamlEncoderAllPropertiesDisabled(b *testing.B) { b.ReportAllocs() logger := logs.NewLogger(). - SetEncoder(shared.YamlEncoderType).ShowLogLevel(false) + SetEncoder(shared.YamlEncoderType).ShowLogLevel(false).SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("YAML encoder", "all-properties-enabled", false, "id", 2) + logger.Debug("YAML encoder", "all-properties-enabled", false, "id", i) } - - fmt.Print("Yaml_Encoder_All_Properties_Disabled: ") } func BenchmarkYamlEncoderAllPropertiesEnabled(b *testing.B) { @@ -84,11 +75,10 @@ func BenchmarkYamlEncoderAllPropertiesEnabled(b *testing.B) { logger := logs.NewLogger(). SetEncoder(shared.YamlEncoderType). ShowLogLevel(true). - AddDateTime(true) + AddDateTime(true). + SetLogFile(initDevNullFile()) for i := 0; i < b.N; i++ { - logger.Debug("YAML encoder", "all-properties-enabled", true, "id", 2) + logger.Debug("YAML encoder", "all-properties-enabled", true, "id", i) } - - fmt.Print("Yaml_Encoder_All_Properties_Enabled: ") } diff --git a/test/functions.go b/test/functions.go index 41969c4..679789b 100644 --- a/test/functions.go +++ b/test/functions.go @@ -38,3 +38,14 @@ func CaptureErrorOutput(f func()) string { _, _ = buf.ReadFrom(r) return buf.String() } + +func initDevNullFile() *os.File { + var err error + // Open /dev/null (or NUL on Windows) to discard output for tiny-logger + devNullFile, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + panic(err) + } + + return devNullFile +} diff --git a/test/loggers_comparison_test.go b/test/loggers_comparison_test.go new file mode 100644 index 0000000..d65a251 --- /dev/null +++ b/test/loggers_comparison_test.go @@ -0,0 +1,83 @@ +package test + +import ( + "testing" + + "github.com/pho3b/tiny-logger/logs" + "github.com/pho3b/tiny-logger/shared" + "github.com/rs/zerolog" + "github.com/sirupsen/logrus" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// 1. Tiny Logger (Project) +func BenchmarkTinyLogger(b *testing.B) { + // Initialize tiny-logger + logger := logs.NewLogger().SetEncoder(shared.JsonEncoderType).AddDateTime(true) + // Redirect to /dev/null to measure logger overhead only, not I/O + logger.SetLogFile(initDevNullFile()) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Simulating a log with a message and a few fields + logger.Info("Benchmark message", "iteration", i, "active", true) + } +} + +// 2. Logrus (Standard-like popular logger) +func BenchmarkLogrus(b *testing.B) { + logger := logrus.New() + logger.Out = initDevNullFile() + logger.SetFormatter(&logrus.TextFormatter{DisableColors: true}) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + logger.WithFields(logrus.Fields{ + "iteration": i, + "active": true, + }).Info("Benchmark message") + } +} + +// 3. Zerolog (Zero Allocation Logger) +func BenchmarkZerolog(b *testing.B) { + logger := zerolog.New(initDevNullFile()) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + logger.Info(). + Any("iteration", i). + Any("active", true). + Msg("Benchmark message") + } +} + +// 4. Zap (Uber's fast logger) +func BenchmarkZap(b *testing.B) { + // Configure Zap to discard output + encoderConfig := zap.NewProductionEncoderConfig() + core := zapcore.NewCore( + zapcore.NewJSONEncoder(encoderConfig), + zapcore.AddSync(initDevNullFile()), + zap.InfoLevel, + ) + logger := zap.New(core) + defer logger.Sync() + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + logger.Info("Benchmark message", + zap.Any("iteration", i), + zap.Any("active", true), + ) + } +} diff --git a/test/mocks.go b/test/mocks.go index ea18b9c..72fc06c 100644 --- a/test/mocks.go +++ b/test/mocks.go @@ -25,6 +25,7 @@ func (m *LoggerConfigMock) GetLogLvlIntValue() int8 { func (m *LoggerConfigMock) GetDateTimeEnabled() (dateEnabled bool, timeEnabled bool) { return m.DateEnabled, m.TimeEnabled } + func (m *LoggerConfigMock) GetColorsEnabled() bool { return m.ColorsEnabled }