Skip to content

Commit 8532d71

Browse files
authored
docs: major README update and adds benchmark comparison test
test: adds logger comparison benchmark tests fix: casting method casts int32 as number and not UTF8 characters docs: updates the README.md file
1 parent 5ababfd commit 8532d71

16 files changed

Lines changed: 342 additions & 103 deletions

Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.PHONY: test test-coverage test-benchmark encoders-benchmark loggers-comparison-benchmark
2+
3+
ARCH=amd64
4+
5+
# Run all tests with coverage profile
6+
test:
7+
CGO_ENABLED=1 GOARCH=${ARCH} go test -cover ./... -race
8+
9+
# Run all tests and Show HTML coverage report in the browser
10+
test-coverage:
11+
CGO_ENABLED=1 GOARCH=${ARCH} go test -cover -coverprofile=coverage.out ./... -race && go tool cover -html=coverage.out
12+
13+
# Run encoders benchmark tests
14+
encoders-benchmark:
15+
CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/encoders_benchmark_test.go ./test/functions.go -bench=. -benchmem -benchtime=5s -cpu=8
16+
17+
# Run commercial loggers benchmark comparison tests
18+
loggers-comparison-benchmark:
19+
CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/loggers_comparison_test.go ./test/functions.go -bench=. -benchmem -benchtime=5s -cpu=8

README.md

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
# Tiny Logger
22

3-
A fast, lightweight, zero-dependency logging solution for Go applications that prioritizes performance and
4-
simplicity.
5-
Compatible with Go version 1.18.x and above
3+
A fast, lightweight, zero-dependency logging solution for Go applications that prioritizes performance and simplicity.
64

7-
## ✅ Key Features
5+
This library is extremely optimized to log loosely-typed data (interfaces), so you won't have to specify concrete types before logging.
86

9-
- **Lightweight**: No external dependencies mean faster builds and smaller binaries <br>
10-
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.
11-
- **Simplicity**: Clean API design with a minimal learning curve. You'll set it up in seconds.
7+
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.
8+
There are many projects that can benefit from having a logging library that is compact, fast, easy to use, and simple to modify.
9+
Since the codebase is so small, it won't take long for you to understand it and modify it at will.
10+
11+
The project is compatible with **Go version 1.18.x** and above.
12+
13+
## Key Features
14+
15+
- **Lightweight**: The library has no dependencies, the code you see is all that runs.
16+
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.
17+
- **Simplicity**: I designed the API to have a minimal learning curve. You'll set it up in seconds.
1218
- **Performance**: The library is benchmarked to be very fast. It implements custom JSON and YAML marshaling
1319
specifically optimized for logging
14-
- Up to 1.4x faster JSON marshaling than `encoding/json`
15-
- Up to 5x faster YAML marshaling than `gopkg.in/yaml.v3`
16-
- **Color Support**: Built-in ANSI color support for terminal output
17-
- **Thread-Safe**: Concurrent-safe logging with atomic operations
18-
- **Time-Optimized**: Efficient date/time print built-int logic with minimal allocations
19-
- **Reliability**: Thoroughly tested with high test coverage
20-
- **Maintainability**: A small, focused codebase makes it easy to understand and modify at will
20+
- Up to 1.4x faster JSON marshaling than `encoding/json`
21+
- Up to 5x faster YAML marshaling than `gopkg.in/yaml.v3`
22+
- **Color Support**: Built-in ANSI color support for terminal output.
23+
- **Thread-Safe**: Concurrent-safe logging with atomic operations.
24+
- **Time-Optimized**: Efficient date/time formatting with minimal allocations.
25+
- **Memory-Efficient**: Heap allocations and log sizes are kept to a minimum to avoid triggering the garbage collector.
2126

22-
## 🎯 Use Examples
27+
## Use Examples
2328

2429
````go
2530
/******************** Basic Logging methods usage ********************/
@@ -64,44 +69,64 @@ logger.SetDateTimeFormat(shared.UnixTimestamp)
6469
logger.Debug("This is my Debug log", "Test arg") // stdout: 1690982143.000000 This is my Debug log Test arg
6570
````
6671

67-
## 📊 Benchmark Results
72+
## Benchmarks
73+
74+
1. Benchmarks of the **loggers-comparison-benchmark** in [Makefile](./Makefile)
75+
76+
**NOTE:** These benchmarks intentionally log loosely-typed data across all four compared libraries.
77+
As mentioned above, libraries like **zerolog** can achieve higher performance when logging strictly-defined data types.
78+
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.
79+
80+
81+
- **OS:** Linux
82+
- **Arch:** AMD64
83+
- **CPU:** 12th Gen Intel(R) Core(TM) i9-12900K
6884

69-
This is the result of running the `./test/benchmark_test.go` benchmark on my machine, (ns/op)times do not include the
70-
terminal graphical visualization time.
85+
| Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op |
86+
| :--- | :--- | :--- | :--- | :--- |
87+
| **TinyLogger** | 17,625,723 | **339.9 ns** | **88 B** | **2** |
88+
| Zerolog | 12,983,034 | 460.2 ns | 232 B | 5 |
89+
| Zap | 10,391,967 | 578.3 ns | 136 B | 2 |
90+
| Logrus | 3,607,248 | 1692 ns | 1241 B | 21 |
7191

72-
| Encoder | Configuration | ns/op | B/op | allocs/op |
73-
|---------------------|--------------------|-------|------|-----------|
74-
| **Default Encoder** | All Properties OFF | 490.3 | 80 | 1 |
75-
| | All Properties ON | 511.2 | 104 | 1 |
76-
| **JSON Encoder** | All Properties OFF | 513.3 | 80 | 1 |
77-
| | All Properties ON | 536.5 | 104 | 1 |
78-
| **YAML Encoder** | All Properties OFF | 535.3 | 80 | 1 |
79-
| | All Properties ON | 557.1 | 104 | 1 |
92+
- **OS:** Darwin (macOS)
93+
- **Arch:** AMD64
94+
- **CPU:** VirtualApple @ 2.50GHz
8095

96+
| Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op |
97+
| :--- | :--- | :--- | :--- | :--- |
98+
| **TinyLogger** | 6,091,185 | **972.9 ns** | **88 B** | **2** |
99+
| Zerolog | 4,922,115 | 1220 ns | 232 B | 5 |
100+
| Zap | 3,938,301 | 1517 ns | 136 B | 2 |
101+
| Logrus | 1,814,809 | 3291 ns | 1241 B | 21 |
81102

82-
## 🤝 Contributing
103+
2. Benchmarks of the **encoders-benchmark** command contained in the [Makefile](./Makefile)
83104

84-
Contributions are welcome, Here's how you can help:
105+
- **OS:** Linux
106+
- **Arch:** AMD64
107+
- **CPU:** 12th Gen Intel(R) Core(TM) i9-12900K
85108

86-
1. Fork the repository
87-
2. Clone your fork:
88-
3. Create a new branch:
109+
| Logger | Iterations | Time / Op | Bytes / Op | Allocs / Op |
110+
| :--- | :--- | :--- | :--- | :--- |
111+
| DefaultEncoder DisabledProperties | 18336217 | 298.7 ns | 88 B | 2 |
112+
| DefaultEncoder EnabledProperties | 18336217 | 334.3 ns | 88 B | 2 |
113+
| JsonEncoder DisabledProperties | 17974824 | 316.0 ns | 88 B | 2 |
114+
| JsonEncoder EnabledProperties | 17488896 | 344.2 ns | 88 B | 2 |
115+
| YamlEncoder DisabledProperties | 17625220 | 342.8 ns | 88 B | 2 |
116+
| YamlEncoder EnabledProperties | 16005187 | 373.3 ns | 88 B | 2 |
89117

90-
```bash
91-
git checkout -b feat/your-feature-name
92-
```
118+
## Contributing
93119

94-
- **Code Style**
95-
- Follow standard Go formatting (`go fmt`)
96-
- Use meaningful variable names
97-
- Add comments for non-obvious code sections
98-
- Write tests for new functionality
120+
Contributions to this project are very welcome, here's how you can do it:
99121

100-
- **Testing**
101-
- Run tests: `make test`
102-
- Run benchmarks: `make test-benchmark`
103-
- Ensure test coverage remains high; it can be checked using `make test-coverage`
122+
1. Fork the repository
123+
2. Clone your fork
124+
3. Create a new branch
125+
```bash git checkout -b your-feature-name```
126+
4. Local Tests
127+
Take a look at the [Makefile](./Makefile).
128+
You can use the commands provided to run `test`, check the `test-coverage` and monitor the library's `benchmarks`.
104129

105-
## 📝 License
130+
## License
106131

107-
MIT License—see [LICENSE](https://mit-license.org/) file for details
132+
MIT License—see [LICENSE](https://mit-license.org/) file for details

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
module github.com/pho3b/tiny-logger
22

3-
go 1.23.2
3+
go 1.24.0
44

55
require (
6+
github.com/rs/zerolog v1.34.0
7+
github.com/sirupsen/logrus v1.9.4
68
github.com/stretchr/testify v1.11.1
9+
go.uber.org/zap v1.27.1
710
gopkg.in/yaml.v3 v3.0.1
811
)
912

1013
require (
1114
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/mattn/go-colorable v0.1.14 // indirect
16+
github.com/mattn/go-isatty v0.0.20 // indirect
1217
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
go.uber.org/multierr v1.11.0 // indirect
19+
golang.org/x/sys v0.40.0 // indirect
1320
)

go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
12
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
5+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
6+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
7+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
8+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
9+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
10+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
11+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
12+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
313
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
414
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15+
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
16+
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
17+
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
18+
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
19+
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
520
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
621
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
22+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
23+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
724
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
825
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
926
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
1027
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
28+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
32+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
1133
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1234
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1335
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/services/constants.go

Lines changed: 0 additions & 5 deletions
This file was deleted.

internal/services/json_marshaler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import (
66
"strconv"
77
)
88

9+
const (
10+
jsonCharOverhead = 80
11+
averageExtraLen = 30
12+
)
13+
914
// JsonLogEntry represents a structured log entry that can be marshaled to JSON format.
1015
// All fields except Message are optional and will be omitted if empty.
1116
type JsonLogEntry struct {

internal/services/json_mashaler_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ func TestJsonMarshaler_Marshal_OnlyTime(t *testing.T) {
143143

144144
m.MarshalInto(buf, entry)
145145
got := buf.String()
146-
// Expecting "ts" key instead of "datetime"
147146
want := `{"level":"info","time":"16:00","msg":"only time"}`
148147
if got != want {
149148
t.Errorf("Marshal() = %q, want %q", got, want)

internal/services/yaml_marshaler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strconv"
77
)
88

9+
const yamlCharOverhead = 70
10+
911
// YamlLogEntry represents a structured log entry that can be marshaled to YAML format.
1012
// All fields except Message are optional and will be omitted if empty.
1113
type YamlLogEntry struct {

logs/encoders/base.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ type baseEncoder struct {
1919
bufferSyncPool sync.Pool
2020
}
2121

22-
// castAndConcatenateInto writes all the given arguments cast to string and concatenated by a white space into the given buffer.
22+
// castAndConcatenateInto writes all the given arguments cast to string and concatenated by a white space
23+
// into the given buffer.
24+
// The function uses the slower fmt.Sprint only for unknown types
2325
func (b *baseEncoder) castAndConcatenateInto(buf *bytes.Buffer, args ...any) {
2426
argsLen := len(args)
2527
buf.Grow(averageWordLen * argsLen)
@@ -32,48 +34,92 @@ func (b *baseEncoder) castAndConcatenateInto(buf *bytes.Buffer, args ...any) {
3234
switch v := arg.(type) {
3335
case string:
3436
buf.WriteString(v)
35-
case rune:
36-
buf.WriteRune(v)
37+
case []byte:
38+
buf.Write(v)
3739
case int:
3840
buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10))
41+
case int8:
42+
buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10))
43+
case int16:
44+
buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10))
45+
case int32:
46+
buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10))
3947
case int64:
4048
buf.Write(strconv.AppendInt(buf.AvailableBuffer(), v, 10))
49+
case uint:
50+
buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10))
51+
case uint8:
52+
buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10))
53+
case uint16:
54+
buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10))
55+
case uint32:
56+
buf.Write(strconv.AppendUint(buf.AvailableBuffer(), uint64(v), 10))
57+
case uint64:
58+
buf.Write(strconv.AppendUint(buf.AvailableBuffer(), v, 10))
59+
case float32:
60+
buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), float64(v), 'f', -1, 32))
4161
case float64:
4262
buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), v, 'f', -1, 64))
4363
case bool:
44-
buf.Write(strconv.AppendBool(buf.AvailableBuffer(), v))
64+
if v {
65+
buf.WriteString("true")
66+
break
67+
}
68+
69+
buf.WriteString("false")
4570
case fmt.Stringer:
4671
buf.WriteString(v.String())
4772
case error:
4873
buf.WriteString(v.Error())
4974
default:
50-
// Using the slower fmt.Sprint only for unknown types
5175
buf.WriteString(fmt.Sprint(v))
5276
}
5377
}
5478
}
5579

5680
// castToString is a fast casting method that returns the given argument as a string.
81+
// It uses the slow fmt.Sprint only for unknown types
5782
func (b *baseEncoder) castToString(arg any) string {
5883
switch v := arg.(type) {
5984
case string:
6085
return v
61-
case rune:
86+
case []byte:
6287
return string(v)
6388
case int:
6489
return strconv.Itoa(v)
90+
case int8:
91+
return strconv.FormatInt(int64(v), 10)
92+
case int16:
93+
return strconv.FormatInt(int64(v), 10)
94+
case int32:
95+
return strconv.FormatInt(int64(v), 10)
6596
case int64:
6697
return strconv.FormatInt(v, 10)
98+
case uint:
99+
return strconv.FormatUint(uint64(v), 10)
100+
case uint8:
101+
return strconv.FormatUint(uint64(v), 10)
102+
case uint16:
103+
return strconv.FormatUint(uint64(v), 10)
104+
case uint32:
105+
return strconv.FormatUint(uint64(v), 10)
106+
case uint64:
107+
return strconv.FormatUint(v, 10)
108+
case float32:
109+
return strconv.FormatFloat(float64(v), 'f', -1, 32)
67110
case float64:
68111
return strconv.FormatFloat(v, 'f', -1, 64)
69112
case bool:
70-
return strconv.FormatBool(v)
113+
if v {
114+
return "true"
115+
}
116+
117+
return "false"
71118
case fmt.Stringer:
72119
return v.String()
73120
case error:
74121
return v.Error()
75122
default:
76-
// Using the slower fmt.Sprint only for unknown types
77123
return fmt.Sprint(v)
78124
}
79125
}

0 commit comments

Comments
 (0)