Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
115 changes: 70 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <br>
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 ********************/
Expand Down Expand Up @@ -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
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
5 changes: 0 additions & 5 deletions internal/services/constants.go

This file was deleted.

5 changes: 5 additions & 0 deletions internal/services/json_marshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion internal/services/json_mashaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions internal/services/yaml_marshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
62 changes: 54 additions & 8 deletions logs/encoders/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
Expand Down
Loading
Loading