nlog is a lightweight, high-performance structured logging library for Go with zero-allocation optimizations, async processing, and flexible configuration.
nlog is designed around immutable logger instances using the Builder pattern, making it inherently thread-safe. Logging is async by default with configurable overflow policies, and the hot path achieves zero allocations.
| Feature | Description |
|---|---|
| Zero-Alloc Hot Path | Entry pooling and level-check-before-allocation eliminate heap allocations |
| Async by Default | Bounded queues with per-level overflow policies (Drop, Block) |
| Immutable Loggers | Builder pattern ensures thread safety without read-path locking |
| Flexible Handlers | Console, File (with rotation), MultiHandler fan-out |
| log/slog Integration | Drop-in slog.Handler adapter for standard library compatibility |
| Structured Logging | Type-safe field constructors (String, Int, Float64, Bool, Time, Duration, Err, Any) |
| JSON & Text Formatters | Zero-copy WriterFormatter interface for both built-in formatters |
| File Rotation | Built-in rotation by size, age, or interval with backup management |
| Telemetry | Runtime statistics for monitoring drops, blocks, and throughput |
| CoarseClock | Optional coarse-grained clock for reduced timestamp overhead |
Go's standard log package is minimal and log/slog focuses on compatibility across backends. nlog fills the gap for applications that need maximum throughput with zero allocations on the hot path, while still offering structured logging, async processing, and flexible output routing — all in a single, cohesive library.
go get github.com/philipp01105/nlogRequires Go 1.24 or later.
The simplest way to use nlog is via the package-level default logger:
package main
import (
"github.com/philipp01105/nlog/logger"
)
func main() {
logger.Info("Application started")
logger.Info("User login",
logger.String("username", "alice"),
logger.Int("user_id", 123),
)
}For more advanced usage, create a custom logger instance with the Builder:
package main
import (
"github.com/philipp01105/nlog/formatter"
"github.com/philipp01105/nlog/handler/consolehandler"
"github.com/philipp01105/nlog/logger"
)
func main() {
ch := consolehandler.NewConsoleHandler(consolehandler.ConsoleConfig{
Async: true,
BufferSize: 1000,
Formatter: formatter.NewTextFormatter(formatter.Config{
IncludeCaller: true,
}),
})
myLogger := logger.NewBuilder().
WithHandler(ch).
WithLevel(logger.DebugLevel).
WithCaller(true).
WithFields(logger.String("service", "api")).
Build()
myLogger.Debug("Debug message with caller info")
myLogger.Info("Info message", logger.Int("count", 42))
}With formatter.NewJSONFormatter, for easy parsing by logstash or Splunk:
{"level":"INFO","message":"JSON formatted log","response_time":0.123,"service":"api","time":"2026-02-18T13:00:00Z","timestamp":"2026-02-18T13:00:00Z"}
nlog encourages structured logging through type-safe field constructors instead of format strings:
logger.Info("User action",
logger.String("username", "alice"),
logger.Int("age", 30),
logger.Int64("user_id", 123456789),
logger.Float64("score", 98.5),
logger.Bool("admin", true),
logger.Time("login_time", time.Now()),
logger.Duration("elapsed", 5*time.Second),
logger.Err(err),
logger.Any("custom", customObject),
)Often it's helpful to have fields always attached to log statements in an application or parts of one. Instead of repeating fields on every line, use With() to create a child logger with persistent context (immutable operation):
requestLogger := logger.With(
logger.String("request_id", "req-12345"),
logger.String("method", "GET"),
)
requestLogger.Info("Processing request", logger.String("path", "/api/users"))
requestLogger.Info("Request completed", logger.Int("status", 200))If you wish to add the calling method as a field, enable caller reporting:
myLogger := logger.NewBuilder().
WithCaller(true).
Build()Note that this does add measurable overhead.
nlog has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
logger.Debug("Useful debugging information.")
logger.Info("Something noteworthy happened!")
logger.Warn("You should probably take a look at this.")
logger.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
logger.Fatal("Bye.")
// Calls panic() after logging
logger.Panic("I'm bailing.")You can set the logging level on a Logger, then it will only log entries with that severity or anything above it:
myLogger := logger.NewBuilder().
WithLevel(logger.InfoLevel). // Default. Will log Info and above.
Build()The built-in logging formatters are:
formatter.NewTextFormatter— Human-readable text output with optional caller info.formatter.NewJSONFormatter— Logs fields as JSON.
Both formatters support the zero-copy WriterFormatter interface for zero-allocation formatting.
You can define your own formatter by implementing the Formatter interface.
nlog ships with multiple handler implementations, organized in sub-packages:
- consolehandler.ConsoleHandler — Writes to stdout/stderr. Async by default.
- filehandler.FileHandler — Writes to files with built-in rotation (by size, age, or interval).
- multihandler.MultiHandler — Fan-out to multiple handlers simultaneously.
- sloghandler.SlogHandler — Drop-in
slog.Handleradapter forlog/slogcompatibility.
import (
"github.com/philipp01105/nlog/handler/consolehandler"
"github.com/philipp01105/nlog/handler/filehandler"
"github.com/philipp01105/nlog/handler/multihandler"
)
ch := consolehandler.NewConsoleHandler(consolehandler.ConsoleConfig{Async: true})
fh, _ := filehandler.NewFileHandler(filehandler.FileConfig{
Filename: "/var/log/app.log",
Async: true,
})
multi := multihandler.NewMultiHandler(ch, fh)
myLogger := logger.NewBuilder().
WithHandler(multi).
Build()
myLogger.Info("This goes to both console and file")Async is the default. To opt out, disable it per handler:
syncHandler := consolehandler.NewConsoleHandler(consolehandler.ConsoleConfig{
Async: false,
})Unlike many logging libraries, file rotation is built-in. Multiple rotation triggers and backup management are supported:
filehandler.NewFileHandler(filehandler.FileConfig{
Filename: "/var/log/app.log",
MaxSize: 100 * 1024 * 1024, // Rotate at 100MB
MaxAge: 7 * 24 * time.Hour, // Rotate after 7 days
RotateInterval: 24 * time.Hour, // Also rotate daily
MaxBackups: 30, // Keep 30 old files
})Control what happens when async queues fill up:
h := consolehandler.NewConsoleHandler(consolehandler.ConsoleConfig{
OverflowPolicy: map[core.Level]handler.OverflowPolicy{
core.DebugLevel: handler.DropNewest,
core.ErrorLevel: handler.Block,
},
BlockTimeout: 100 * time.Millisecond,
DrainTimeout: 5 * time.Second,
})Available policies:
DropNewest— Drop incoming log entry (default for DEBUG/INFO/WARN)DropOldest— Remove oldest entry from queueBlock— Block caller with timeout, fallback to sync write (default for ERROR)
Monitor logging behavior in real-time to detect slow I/O, tune buffer sizes, and observe application load:
stats := handler.Stats()
fmt.Printf("Processed: %d\n", stats.ProcessedTotal)
fmt.Printf("Dropped: %d\n", stats.DroppedTotal[core.InfoLevel])
fmt.Printf("Blocked: %d\n", stats.BlockedTotal)nlog provides a drop-in slog.Handler adapter for seamless integration with Go's standard log/slog package:
package main
import (
"log/slog"
"github.com/philipp01105/nlog/core"
"github.com/philipp01105/nlog/formatter"
"github.com/philipp01105/nlog/handler/consolehandler"
"github.com/philipp01105/nlog/handler/sloghandler"
)
func main() {
ch := consolehandler.NewConsoleHandler(consolehandler.ConsoleConfig{
Async: false,
Formatter: formatter.NewTextFormatter(formatter.Config{}),
})
sh := sloghandler.NewSlogHandler(ch, core.InfoLevel)
logger := slog.New(sh)
logger.Info("Hello from slog", "user", "alice", "count", 42)
logger.Warn("Something might be wrong", "component", "auth")
ch.Close()
}Logger instances are immutable after construction via the Builder pattern, making them inherently safe for concurrent use. Async handlers use bounded queues with configurable overflow policies. Shutdown is handled via an idempotent Close() with timeout-based queue draining.
nlog achieves zero allocations on the hot path through entry pooling, level-check-before-allocation, and the WriterFormatter zero-copy interface.
BenchmarkInfoNoFields 144 ns/op 0 B/op 0 allocs/op
BenchmarkInfoWith2Fields 202 ns/op 0 B/op 0 allocs/op
BenchmarkJSON 445 ns/op 0 B/op 0 allocs/op
BenchmarkZeroAllocFiltered 0.3 ns/op 0 B/op 0 allocs/op
BenchmarkFilteredDebug 8.8 ns/op 0 B/op 0 allocs/op
BenchmarkMultiGoroutineContention 29 ns/op 5 B/op 0 allocs/op
BenchmarkQueueFullStress 18 ns/op 0 B/op 0 allocs/op
Filtered logs (level too low) cause zero allocations — the level check happens before GetEntry():
myLogger := logger.NewBuilder().
WithLevel(logger.InfoLevel).
Build()
myLogger.Debug("filtered") // 0.3 ns/op, 0 allocs| Package | Description |
|---|---|
core/ |
Core types (Entry, Field, Level) shared across packages |
logger/ |
Main Logger API, Builder, and convenience functions |
handler/ |
Handler interface, StatsProvider, OverflowPolicy, and Stats types |
handler/consolehandler/ |
Console handler (sync/async) writing to io.Writer |
handler/filehandler/ |
File handler (sync/async) with rotation support |
handler/multihandler/ |
Fan-out handler dispatching to multiple children |
handler/sloghandler/ |
Adapter for log/slog compatibility |
formatter/ |
Formatter interface and implementations (Text, JSON, WriterFormatter) |
Run tests:
go test ./...Run benchmarks:
go test -bench=. -benchmem ./...This project is open source and available under the MIT License.