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
}