

grlog
“Production-ready structured logging for Go — zero dependencies”
A stdlib-only Go logging library with typed zero-allocation fields, pluggable sinks and formatters, async logging with overflow policies, self-healing file rotation, deterministic JSON, and a log/slog adapter. One package, no go.sum.
Installation Guide
A single Go module with no transitive dependencies — installation is one command on every platform.
Requirements
grlog uses log/slog and errors.Join, both introduced in Go 1.21. That is the only requirement.
https://go.dev/dl/go.mod declares the module path and Go version — nothing else. There is no go.sum because there is nothing to checksum.
Add the module
Fetch the latest tagged release into your project.
go get github.com/gourdian25/grlogImport and use
Everything lives in one package — logger, fields, sinks, formatters, and the slog adapter.
Pin a version (optional)
grlog follows semver; v0.1.0 contains breaking changes from v0.0.1 (documented in the CHANGELOG with migration notes).
go get github.com/gourdian25/grlog@v0.1.0Quick Start Guide
From go get to structured production logs in three steps.
Install the module
One go get — grlog has zero third-party dependencies, so this adds exactly one line to your go.mod and nothing to go.sum.
go get github.com/gourdian25/grlog💡 Tips
- •Requires Go 1.21+ (for log/slog and errors.Join)
Create a logger — and defer Close
NewDefaultLogger gives you INFO level, plain-text output to stdout, and caller info. The deferred Close matters: it flushes async buffers and closes sinks, and it's guaranteed to lose nothing.
logger := grlog.NewDefaultLogger()
defer logger.Close() // flushes buffers; safe to call from any derived view
logger.Info("Application started")
logger.Warn("High memory usage detected")💡 Tips
- •Close is idempotent — calling it on the parent and a derived view is safe
Log with typed fields
Structured data goes in typed fields, not maps or format strings — construction is zero-allocation and the output is machine-parseable with either formatter.
logger.Info("User authenticated",
grlog.String("user_id", "12345"),
grlog.Int("attempts", 3),
grlog.Duration("latency", 45*time.Millisecond),
grlog.Err(err), // nil-safe: omitted entirely when err == nil
)
// 2026-07-05 18:04:12.801 [INFO] main.go:24:main User authenticated {user_id=12345, attempts=3, latency=45ms}You're all set! Check out the detailed usage guide below for more advanced features.
Usage Guide
From default logging to async pipelines, rotation, per-request views, and slog interop.
Levels and filtering
Six levels: DEBUG < INFO < WARN < ERROR < FATAL < OFF. The level is stored atomically, so filtering is lock-free and SetLevel is safe from any goroutine at any time. Filtered-out calls cost ~2.7ns and zero allocations.
logger := grlog.NewLogger(grlog.WithLevel(grlog.INFO))
defer logger.Close()
logger.Debug("not written — below INFO")
logger.SetLevel(grlog.DEBUG) // runtime change, e.g. from an admin endpoint
logger.Debug("now it is")
level, _ := grlog.ParseLogLevel("WARN") // from env/config; accepts "WARNING" too
logger.SetLevel(level)Notes
- Fatal / Fatalf log, close the logger (flushing async buffers), then os.Exit(1)
Typed fields vs format strings
Both styles exist. Typed fields keep logs structured and fast; the printf-style variants (Infof, Errorf, …) are there for quick human-only messages.
// Structured — preferred: parseable, zero-alloc construction
logger.Error("payment failed",
grlog.String("order_id", "ord_9182"),
grlog.Float64("amount", 49.99),
grlog.Err(err),
)
// Formatted — fine for simple messages, but the data is trapped in the string
logger.Errorf("payment failed for order %s: %v", orderID, err)Notes
- Available constructors: String, Int, Int64, Uint64, Float64, Bool, Duration, Time, Err, Any
JSON output for production
Swap the formatter for machine-readable logs. grlog's JSON is deterministic — reserved keys first, then custom fields sorted, then your fields in call order — and always valid, even for NaN floats or invalid UTF-8.
logger := grlog.NewLogger(
grlog.WithLevel(grlog.INFO),
grlog.WithSink(grlog.NewStdoutSink(grlog.JSONFormat())),
)
defer logger.Close()
logger.Info("request served", grlog.Int("status", 200))
// {"timestamp":"2026-07-05T18:04:12.801Z","level":"INFO","message":"request served","caller":"main.go:18:main","status":200}API Reference
The complete public surface: constructors, options, logging methods, fields, sinks, formatters, and the slog adapter.
NewLogger / NewDefaultLogger
NewLogger builds a logger from functional options (defaults: DEBUG level, stdout sink with plain formatting, caller info on, sync). NewDefaultLogger is the one-liner production preset: INFO level, plain stdout, caller info.
Signature
func NewLogger(opts ...LoggerOption) *LoggerParameters
WithLevel(level)LoggerOptionMinimum level: DEBUG, INFO, WARN, ERROR, FATAL, or OFF
WithSink(sink)LoggerOptionAdd an output sink (repeatable)
WithAsync(bufferSize)LoggerOptionEnable async logging with a buffered queue (> 0)
WithOverflowPolicy(policy)LoggerOptionFull-queue behavior: OverflowSyncFallback (default), OverflowBlock, OverflowDrop
WithCaller(enabled) / WithCallerSkip(n)LoggerOptionInclude file:line:function; skip extra frames when wrapping
WithContextFields(fields...)LoggerOptionFields stamped on every entry (service, version, env)
WithContextExtractor(fn)LoggerOptionCustom context.Context → []Field extraction for WithContext
WithSampler(every, maxLevel)LoggerOptionKeep 1-in-N entries at or below maxLevel
WithErrorHandler(fn)LoggerOptionCallback for sink write failures (default: rate-limited stderr)
Returns
*LoggerExamples
logger := grlog.NewDefaultLogger()logger := grlog.NewLogger(grlog.WithLevel(grlog.INFO), grlog.WithAsync(10000))Debug / Info / Warn / Error / Fatal (+ *f variants)
The logging methods. Each takes a message plus variadic typed fields; the printf-style variants take a format string instead. Fatal logs, closes the logger (flushing async buffers), then exits with status 1.
Signature
func (l *Logger) Info(msg string, fields ...Field)Parameters
msgstringRequiredThe log message
fields...FieldTyped structured fields
Returns
—Examples
logger.Info("cache warmed", grlog.Int("keys", 15000))logger.Errorf("connect failed: %v", err)logger.Fatal("config invalid", grlog.Err(err)) // flushes, then os.Exit(1)Field constructors
Typed key-value constructors — all zero-allocation. Err takes only an error and uses the key "error"; Err(nil) produces a field that formatters skip entirely. Any is the reflection-based fallback for arbitrary values.
Signature
String(k, v) | Int(k, v) | Int64(k, v) | Uint64(k, v) | Float64(k, v) | Bool(k, v) | Duration(k, v) | Time(k, v) | Err(err) | Any(k, v)Returns
FieldExamples
grlog.String("user_id", id)grlog.Duration("latency", elapsed)grlog.Err(err) // nil-safegrlog.Any("payload", req) // reflection fallback — use sparinglyWith / WithContext
Derive lightweight logger views. With attaches permanent fields; WithContext extracts request_id / trace_id / user_id stored via the typed context helpers, then runs any registered custom extractors. Views share sinks, level, and the async machinery with the parent.
Signature
func (l *Logger) With(fields ...Field) *Logger | func (l *Logger) WithContext(ctx context.Context) *LoggerReturns
*Logger (view onto the same core)Examples
dbLog := logger.With(grlog.String("component", "db"))reqLog := logger.WithContext(ctx)Context helpers
Store correlation IDs in a context.Context under private typed keys (collision-proof — raw string keys are deliberately not read). WithContext surfaces them as request_id, trace_id, and user_id fields.
Signature
ContextWithRequestID(ctx, id) | ContextWithTraceID(ctx, id) | ContextWithUserID(ctx, id)Returns
context.ContextExamples
ctx = grlog.ContextWithRequestID(ctx, uuid.NewString())ctx = grlog.ContextWithTraceID(ctx, span.TraceID().String())SetLevel / GetLevel / ParseLogLevel
Runtime level control. The level is an atomic — SetLevel is safe from any goroutine and takes effect immediately. ParseLogLevel converts config/env strings (case-insensitive; accepts WARNING as WARN).
Signature
func (l *Logger) SetLevel(level LogLevel) | func ParseLogLevel(s string) (LogLevel, error)Returns
LogLevel / errorExamples
level, err := grlog.ParseLogLevel(os.Getenv("LOG_LEVEL"))logger.SetLevel(grlog.DEBUG) // e.g. from an admin endpointAddSink / RemoveSink / Stats / Close
Runtime sink management (copy-on-write — safe during concurrent logging, visible to all views), loss observability, and shutdown. Close stops the async worker, drains the queue completely, and closes every sink; it is idempotent across all views.
Signature
func (l *Logger) AddSink(s LogSink) | RemoveSink(s LogSink) | Stats() LoggerStats | Close() errorReturns
LoggerStats{DroppedEntries, SampledEntries} / errorExamples
logger.AddSink(fileSink) // start file logging at runtimedropped := logger.Stats().DroppedEntriesdefer logger.Close()NewWriterSink / NewStdoutSink
Write formatted entries to any io.Writer with mutex-guarded thread safety. NewStdoutSink is the cloud-native default (container runtimes collect stdout). nil arguments fall back to os.Stdout and PlainFormat.
Signature
func NewWriterSink(w io.Writer, f Formatter) LogSink | func NewStdoutSink(f Formatter) LogSinkReturns
LogSinkExamples
grlog.NewStdoutSink(grlog.JSONFormat())grlog.NewWriterSink(os.Stderr, grlog.PlainFormat())grlog.NewWriterSink(&buf, nil) // capture logs in testsNewFileSink
File logging with size-based rotation. Backup names are collision-proof (nanosecond timestamp + sequence suffix); failed rotations self-heal (the sink reopens and keeps writing, and the triggering entry is still written); compression is atomic.
Signature
func NewFileSink(config FileSinkConfig) (LogSink, error)Parameters
FilenamestringDefault: appBase name without extension
DirstringDefault: logsLog directory
MaxBytesint64Default: 10MBRotate when the file reaches this size
BackupCountintDefault: 5Rotated files to keep
MaxAgetime.DurationDefault: 0Delete backups older than this (0 = disabled)
CompressboolDefault: falseGzip rotated backups in the background, atomically
FormatterFormatterDefault: PlainFormat()Entry formatter
Returns
(LogSink, error)Examples
sink, err := grlog.NewFileSink(grlog.FileSinkConfig{Filename: "app", Compress: true})NewMultiSink / NewLeveledSink / NewCustomSink
Composition sinks. MultiSink fans out to all children and aggregates failures with errors.Join (one failing sink never blocks the rest). LeveledSink drops entries below a minimum level before they reach the wrapped sink. CustomSink turns any function into a sink — databases, queues, metrics.
Signature
NewMultiSink(sinks ...LogSink) | NewLeveledSink(sink, minLevel) | NewCustomSink(writeFn) | NewCustomSinkWithClose(writeFn, closeFn)Returns
LogSinkExamples
grlog.NewMultiSink(stdout, grlog.NewLeveledSink(errFile, grlog.ERROR))grlog.NewCustomSink(func(e grlog.LogEntry) error { return queue.Publish(e) })PlainFormat / JSONFormat
The built-in formatters, both implementing an allocation-free append path used with pooled buffers. PlainFormat: human-readable lines with millisecond timestamps. JSONFormat: compact deterministic JSON (RFC3339Nano timestamps); PrettyPrint and static CustomFields available on the JSONFormatter struct.
Signature
func PlainFormat() Formatter | func JSONFormat() FormatterReturns
FormatterExamples
grlog.NewStdoutSink(grlog.JSONFormat())&grlog.JSONFormatter{PrettyPrint: true, CustomFields: map[string]interface{}{"service": "api"}}NewSlogHandler
A slog.Handler backed by a grlog logger. Levels map Debug→DEBUG, Info→INFO, Warn→WARN, Error→ERROR; slog groups flatten into dotted key prefixes; attributes convert to typed fields by Kind; caller info is recovered from the record's program counter. Respects the logger's level, closed state, and sampler.
Signature
func NewSlogHandler(logger *Logger) slog.HandlerReturns
slog.HandlerExamples
slog.SetDefault(slog.New(grlog.NewSlogHandler(logger)))slog.Info("msg", slog.Group("req", slog.String("id", "1"))) // → field req.idExamples Gallery
Real-world code examples and use cases
Minimal service logger
beginnerDefaults that make sense everywhere: INFO level, human-readable stdout, caller info, and a guaranteed flush on exit.
package main
import "github.com/gourdian25/grlog"
func main() {
logger := grlog.NewDefaultLogger()
defer logger.Close()
logger.Info("service starting", grlog.String("version", "1.4.2"))
}Capture logs in tests
beginnerNewWriterSink accepts any io.Writer — point it at a bytes.Buffer and assert on output. (grlog's own suite does exactly this.)
var buf bytes.Buffer
logger := grlog.NewLogger(
grlog.WithSink(grlog.NewWriterSink(&buf, grlog.JSONFormat())),
grlog.WithCaller(false), // deterministic output for assertions
)
doTheThing(logger)
logger.Close()
if !strings.Contains(buf.String(), `"message":"thing done"`) {
t.Errorf("expected log entry, got: %s", buf.String())
}Frequently Asked Questions
6 questions answered
Still have questions?
Check out the source code or open an issue on GitHub
AI-readable content
Learn more →This content is available via the AI Content API as JSON or token-efficient Markdown. Feed it directly into LLM workflows.
/api/content/docs/grlogRelated Content
Explore related articles, projects, and tools.