https://github.com/semihalev/zlog
A small, very fast, truly zero-allocation structured logger for Go
https://github.com/semihalev/zlog
golang logger logging zerolog
Last synced: about 1 month ago
JSON representation
A small, very fast, truly zero-allocation structured logger for Go
- Host: GitHub
- URL: https://github.com/semihalev/zlog
- Owner: semihalev
- License: mit
- Created: 2018-06-27T15:13:00.000Z (almost 8 years ago)
- Default Branch: main
- Last Pushed: 2026-04-25T13:25:28.000Z (2 months ago)
- Last Synced: 2026-04-25T15:24:56.232Z (2 months ago)
- Topics: golang, logger, logging, zerolog
- Language: Go
- Homepage:
- Size: 93.8 KB
- Stars: 5
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# zlog
[](https://pkg.go.dev/github.com/semihalev/zlog/v2)
[](https://goreportcard.com/report/github.com/semihalev/zlog)
[](LICENSE)
A small, very fast, **truly zero-allocation** structured logger for Go.
Apple M5, Go 1.23+:
```
BenchmarkUltimateLogger 15.9 ns/op 0 B/op 0 allocs/op
BenchmarkUltimateLoggerParallel 5.2 ns/op 0 B/op 0 allocs/op
BenchmarkStructuredLogger 35.6 ns/op 0 B/op 0 allocs/op
BenchmarkStructuredLoggerParallel 10.5 ns/op 0 B/op 0 allocs/op
BenchmarkStructured + 5 fields 45.2 ns/op 0 B/op 0 allocs/op
BenchmarkStructured + 10 fields 76.7 ns/op 0 B/op 0 allocs/op
BenchmarkRealWorld → TerminalWriter 26.5 ns/op 0 B/op 0 allocs/op
BenchmarkDisabledDebug 0.23 ns/op 0 B/op 0 allocs/op
```
The interesting number is the last column on every row: every logging path is genuinely 0 allocs/op once the buffer pool is warm, including the colored terminal output. Parallel benchmarks are *faster* than serial because each `GOMAXPROCS` slot keeps its own pool localcache and there's no contention on a sequence counter.
## Install
```bash
go get github.com/semihalev/zlog/v2
```
Requires Go 1.23+.
## Quick start
```go
package main
import "github.com/semihalev/zlog/v2"
func main() {
log := zlog.NewStructured()
log.SetWriter(zlog.StdoutTerminal())
log.Info("server started",
zlog.String("addr", ":8080"),
zlog.Int("workers", 4),
)
log.Warn("slow query",
zlog.String("table", "users"),
zlog.Float64("duration_ms", 327.4),
)
log.Error("db connection lost",
zlog.String("err", "i/o timeout"),
zlog.Int("retry", 3),
)
}
```
```
INFO [04-25|15:51:30] server started addr=:8080 workers=4
WARN [04-25|15:51:30] slow query table=users duration_ms=327.4
ERROR [04-25|15:51:30] db connection lost err="i/o timeout" retry=3
```
## Logger types
| Type | Purpose | Cost |
|---|---|---|
| `zlog.NewUltimateLogger()` | Bare-message hot path. No fields. | ~16 ns/op serial, ~5 ns/op parallel |
| `zlog.NewStructured()` | Typed fields, the recommended API. | ~36 ns/op + ~10 ns per extra field |
| `zlog.New()` | Plain `Logger`. Same shape as Ultimate. | ~21 ns/op |
For most apps, `NewStructured()` is the right choice: zero alloc, typed fields, ~26ns end-to-end when the writer is a `TerminalWriter` (that path is direct-text, no binary intermediate).
## Fields
```go
log.Info("event",
zlog.String("name", "Alice"),
zlog.Int("age", 30),
zlog.Int64("id", 123456789),
zlog.Uint("count", 42),
zlog.Uint64("total", 9999999),
zlog.Float32("score", 98.5),
zlog.Float64("pi", 3.14159),
zlog.Bool("active", true),
zlog.Bytes("data", []byte{0x01, 0x02, 0x03}),
)
```
All field constructors are inlinable and allocation-free.
There's also a key/value-pair API for the global helper that's friendlier when you have `any` values:
```go
zlog.Info("user logged in", "username", "alice", "user_id", 12345)
```
The global helpers (`zlog.Info`, `zlog.Warn`, ...) accept either typed `Field` values or alternating key/value pairs.
## Writers
- `zlog.StdoutTerminal()` / `zlog.StderrTerminal()` — colored, padded, human-readable terminal output. The structured logger detects this writer and formats text directly into a pooled buffer (no binary intermediate).
- `zlog.StdoutWriter()` / `zlog.StderrWriter()` — raw binary output to stdout/stderr.
- `zlog.NewLogfmtWriter(io.Writer)` — `key=value` text format on top of the binary log.
- `zlog.NewMMapWriter(path, size)` — memory-mapped file with no per-write syscall (Linux/macOS/Windows).
- `zlog.NewAsyncWriter(io.Writer, bufSize)` — lock-free ring buffer with worker drains; for fan-out at high throughput.
- Any `io.Writer` works. The package writes a compact binary record; the terminal/logfmt writers are the supported decoders.
```go
mmap, _ := zlog.NewMMapWriter("/var/log/app.log", 100*1024*1024) // 100 MB
defer mmap.Close()
log := zlog.NewStructured()
log.SetWriter(mmap)
```
## Levels
```go
log := zlog.NewStructured()
log.SetLevel(zlog.LevelWarn) // Only Warn / Error / Fatal are emitted.
if log.GetLevel() <= zlog.LevelDebug {
// expensive debug computation only when needed
}
```
A disabled level call is ~0.23 ns/op (a single atomic load + compare).
## Terminal output
```
DEBUG [01-02|15:04:05] starting up
INFO [01-02|15:04:05] server initialized
WARN [01-02|15:04:05] config not found, using defaults
ERROR [01-02|15:04:05] db connection failed error=timeout retry=3
```
Colors:
- DEBUG → cyan
- INFO → green
- WARN → yellow
- ERROR → red
- FATAL → magenta
Color is auto-detected from the underlying `*os.File` (TTY check). It also respects `NO_COLOR` and `TERM=dumb`. To force-disable in code:
```go
tw := zlog.NewTerminalWriter(os.Stdout).(*zlog.TerminalWriter)
tw.SetColorEnabled(false)
log.SetWriter(tw)
```
The terminal writer caches the formatted timestamp by Unix second, so `time.Time` decomposition runs once per second, not per log line.
### Windows
ANSI colors work on Windows 10 (build 14393+) automatically. On older Windows or when output isn't a TTY, plain text is used. Same env vars (`NO_COLOR`, `TERM=dumb`) apply.
## How it stays zero-alloc
A few of the things that matter:
- Every record is built into a `sync.Pool`-backed buffer indexed by a power-of-two size class. Allocation only happens when the pool is cold.
- The "small message → stack buffer" fast path that traditional loggers use was deliberately removed: passing a stack array to an `io.Writer.Write` interface call forces it onto the heap. The pool path is the actual zero-alloc path.
- Field encoding writes int/float values as a single `*(*uint64)(unsafe.Pointer(&buf[pos])) = ...` store in native byte order — one instruction per numeric field, not eight byte stores.
- The structured logger detects when its writer is a `*TerminalWriter` and formats text directly into the pooled buffer, skipping the binary encode → re-decode round-trip. Type assertion is ~1ns; the saving is ~30-40ns.
- Wall-clock timestamps are computed as `baseWallNs + (nanotime() - baseMonoNs)`, where `baseWallNs` and `baseMonoNs` are sampled once at `init()`. One VDSO call per log, ~5ns. Drifts from the kernel's wall clock if NTP adjusts the system time after init — fine for log timestamps.
- Escape-detection scans use a 256-byte classifier table (Go compiles byte-LUT loads to NEON `tbl` / SSSE3 `pshufb` on supported architectures).
- No `runtime.exit`, no panic-recovery in hot paths, no `defer` in the terminal writer's `Write`.
If you change a logger or writer and start seeing 1 alloc/op, run `go build -gcflags='-m=2'` and look for `escapes to heap` on the buffer — that's almost always an interface boundary somewhere.
## Binary log format
The binary record on disk / on the wire is:
```
0..3 magic "ZLOG" (uint32, little-endian)
4 version (1)
5 level (1)
6..13 unix nanoseconds (uint64, native order)
14..15 msgLen (uint16, native order)
16+ message bytes
optional: 1-byte fieldCount, then fields
```
Each field is `keyLen(1) + key + type(1) + value`. Numeric values are 4 or 8 bytes native order. String / bytes values are `len(uint16, native) + payload`.
Native byte order is intentional: only this package's writers consume the binary form, so a `bswap` per field would buy nothing. The binary form is **not** stable across machines with different endianness — if you ship binary logs over the wire, decode on the producer's architecture.
## Testing
```bash
go test -race ./...
go test -bench=. -benchmem ./...
```
CI runs build + race + zero-alloc verification on Linux / macOS / Windows.
## License
MIT — see [LICENSE](LICENSE).