Skip to content

lbtsm/go-logger

Repository files navigation

go-logger

Production-grade structured logger for Go services. Built on the standard library's log/slog so any consumer of slog can use it as a drop-in.

What you get

  • Structured — JSON or text, key=value attrs, child loggers via .With(...).
  • Rotating files — size / age / backup count / gzip via lumberjack.
  • Multi-sink — stdout + file + custom writer; each can have its own level and encoder.
  • Async writes — bounded queue, drop-or-block policy. Hot path is ~100 ns/op and zero allocs.
  • Samplingn records per (level, message) per second; bounds log volume in hot loops.
  • Hooks — fire Slack/Sentry/custom webhooks on ERROR (or any level set). AsyncHook adapter so a slow webhook never slows logging.
  • slog-compatible — returns *slog.Logger, so the rest of your code uses the standard API and OpenTelemetry / log/slog middleware works.

Install

go get github.com/lbtsm/go-logger

Go 1.21+ required (log/slog).

Quick start

import logger "github.com/lbtsm/go-logger"

lg := logger.NewProduction("/var/log/app.log")
defer lg.Close()

lg.Info("server started", "port", 8080)
lg.Error("upstream failed", "addr", "10.0.0.1:5432", "err", err)

Custom configuration

lg := logger.New(
    logger.WithLevel(slog.LevelInfo),
    logger.WithStdout(logger.FormatJSON),
    logger.WithFile("/var/log/app.log",
        logger.WithMaxSizeMB(100),
        logger.WithMaxBackups(7),
        logger.WithMaxAgeDays(30),
        logger.WithCompress(),
    ),
    logger.WithAsync(1024),         // queue size; drop policy via WithAsyncDropOnFull
    logger.WithSampler(100),        // 100 records / msg / sec
    logger.WithFields(slog.String("svc", "monitor"), slog.String("env", "prod")),
    logger.WithHook(myHook),
)
defer lg.Close()

Hooks

// Built-in HTTP webhook (Slack/Discord/custom).
hook := logger.NewWebhookHook("https://hooks.slack.com/services/XXX",
    logger.WithWebhookLevels(slog.LevelError),
    logger.WithWebhookTimeout(5*time.Second),
)

// Non-blocking dispatch.
async := logger.NewAsyncHook(hook, logger.WithAsyncHookQueueSize(256))
defer async.Close()

lg := logger.New(logger.WithHook(async))

Implement the Hook interface for custom destinations:

type Hook interface {
    Levels() []slog.Level
    Fire(ctx context.Context, r slog.Record) error
}

Convenience constructors

lg := logger.NewProduction("/var/log/app.log") // JSON to stdout+file, async, INFO
lg := logger.NewDevelopment()                  // text to stdout, DEBUG, source on

Package-level default

logger.SetDefault(logger.NewProduction("/var/log/app.log"))
logger.Info("hello", "k", "v")  // uses default

SetDefault is safe to call concurrently with Info / Warn / etc.

Performance

Benchmarks on Apple M3, Go 1.25:

benchmark ns/op allocs/op
BenchmarkAsyncWriter_Write ~101 0
BenchmarkSampling_HotPath ~20 0

Combined with slog's own zero-alloc JSON encoder, a full lg.Info("msg", "k", v) typically runs in a few hundred nanoseconds on warm CPU paths.

Architecture

              ┌──────────────┐
slog.Logger ─▶│   Handler    │──────▶ sink 1 (slog.JSONHandler) ─▶ AsyncWriter ─▶ lumberjack ─▶ file
              │  (fan-out)   │──────▶ sink 2 (slog.TextHandler) ───────────────▶ stdout
              │              │──────▶ hook 1 (WebhookHook)
              └──────────────┘──────▶ hook 2 (AsyncHook → custom)
  • Handler is a slog.Handler that fans out to N sinks and hooks.
  • Each sink is itself a slog.Handler (JSON or Text), optionally wrapped in SamplingHandler.
  • File sinks may be wrapped in AsyncWriter (background goroutine, bounded channel).
  • Hooks run synchronously by default; wrap with AsyncHook for fire-and-forget.

Lifecycle

Always call lg.Close() before exit to flush async buffers and close files. For os.Exit paths (panic recovery, fatal errors), call Close in your shutdown hook or use a defer.

License

MIT

About

go-logger

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages