5 Go Loggers That Will Replace Your Sad Little fmt.Println Habit
Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product. I still println-debug. Not always, but my git history has the receipts. The trouble is, the second you ship anything that runs longer than your patience, that habit stops scaling. You start needing real logs: structured, leveled, stored somewhere that survives a terminal restart. fmt.Println("here") fmt.Println("here 2") fmt.Println("HEREEEEEEE") fmt.Println("why") So I picked five Go logging libraries, checked each, and leaned on the feature the library is actually known for. Not Info("hello") warmed over. Zap is what happens when Uber engineers look at logging and go "yes but what if it allocated zero bytes." It's blazing fast, structured-by-default, and has two flavors: Logger (typed fields, zero-allocation fast path) and SugaredLogger (slightly slower, lets you breathe). Signature feature: AtomicLevel — flip the log level at runtime without restarting your service. Wire it to an HTTP handler and you've got a "turn on debug logs in prod" switch in five lines. package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func main() { // AtomicLevel: flip the log level at runtime (e.g. from a /debug endpoint). atom := zap.NewAtomicLevelAt(zap.InfoLevel) logger := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stderr), atom, ), zap.AddCaller()) defer logger.Sync() // Typed-field API — zero-allocation fast path. logger.Info("typed fields are fast", zap.String("user_id", "u_42"), zap.Int("attempt", 3), ) // Named + With: hierarchical name and bound fields on every line. reqLog := logger.Named("http").With(zap.String("request_id", "req_abc123")) reqLog.Info("handling request") logger.Debug("you will NOT see this — level is INFO") atom.SetLevel(zap.DebugLevel) logger.Debug("now you see me — atomic level flipped to DEBUG") } Use it when: you're building something high-throughput and an extra microsecond per log line offends you on a spiritual level. Skip it when: you just want to print things and go to bed. / zap ⚡ zap Blazing fast, structured, leveled logging in Go. Installation go get -u go.uber.org/zap Note that zap only supports the two most recent minor versions of Go. Quick Start In contexts where performance is nice, but not critical, use the SugaredLogger. It's 4-10x faster than other structured logging packages and includes both structured and printf-style APIs. logger, _ := zap.NewProduction() defer logger.Sync() // flushes buffer, if any sugar := logger.Sugar() sugar.Infow("failed to fetch URL", // Structured context as loosely typed key-value pairs. "url", url, "attempt", 3, "backoff", time.Second, ) sugar.Infof("Failed to fetch URL: %s", url) When performance and type safety are critical, use the Logger. It's even faster than the SugaredLogger and allocates far less, but it only… View on GitHub Zerolog said "what if logging, but make it method chains?" The API is delightful. The performance is roughly on par with Zap. The JSON is pretty. Signature features: the chained typed builders (.Str().Int().Dur().IPAddr()...), ConsoleWriter for pretty colored dev output, Hooks for cross-cutting concerns, and built-in Sample for rate-limiting hot loops. package main import ( "errors" "net" "os" "time" "github.com/rs/zerolog" ) // Hooks fire on every log line — perfect for tagging alerts, adding trace IDs, etc. type alertHook struct{} func (alertHook) Run(e *zerolog.Event, level zerolog.Level, _ string) { if level >= zerolog.ErrorLevel { e.Bool("alert", true) } } func main() { // ConsoleWriter: colored, human-readable output for local dev. console := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.Kitchen} pretty := zerolog.New(console).With().Timestamp().Caller().Logger() pretty.Warn().Str("user", "alice").Int("attempts", 3).Msg("retrying") // JSON for production — sub-logger with bound fields + hook. jsonLog := zerolog.New(os.Stdout). With().Timestamp().Str("service", "api"). Logger().Hook(alertHook{}) jsonLog.Error(). Err(errors.New("db connection refused")). IPAddr("peer", net.ParseIP("10.0.0.1")). Dur("elapsed", 142*time.Millisecond). Msg("write failed") // Sampling: keep 1-in-N lines on a hot path to spare disk. sampled := jsonLog.Sample(&zerolog.BasicSampler{N: 10}) for i := 0; i < 25; i++ { sampled.Info().Int("i", i).Msg("hot loop log") } } Use it when: you want Zap-level speed but you also want your code to look nice on a screenshot. Skip it when: you have a deep, unresolved fear of method chaining. / zerolog Zero Allocation JSON Logger The zerolog package provides a fast and simple logger dedicated to JSON output. Zerolog's API is designed to provide both a great developer experience and stunning performance. Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection. Uber's zap library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance. To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) zerolog.ConsoleWriter. Who uses zerolog Find out who uses zerolog and add your company / project to the list. Features Blazing fast Low to zero allocation Leveled logging Sampling Hooks Contextual fields context.Context integration Integration with net/http JSON and CBOR encoding formats Pretty logging for development … View on GitHub Logrus is the logger that taught a generation of Gophers that logs could be structured. It's the most-starred Go logger on GitHub. It's also officially in maintenance mode, the author isn't adding features, just keeping the lights on. Treat it like a beloved family sedan: still runs great, won't win any races. Signature feature: Hooks. Logrus popularized the "fire a side-effect on every matching log line" pattern. Every Sentry / Slack / syslog adapter in the Go world is some flavor of this: package main import ( "errors" "os" "github.com/sirupsen/logrus" ) // A Hook fires on a configured set of levels — the logrus way to ship // errors to Sentry, post warnings to Slack, etc. type AlertHook struct{} func (h *AlertHook) Levels() []logrus.Level { return []logrus.Level{ logrus.WarnLevel, logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel, } } func (h *AlertHook) Fire(entry *logrus.Entry) error { _, _ = os.Stderr.WriteString("[ALERT] " + entry.Level.String() + ": " + entry.Message + "\n") return nil } func main() { logrus.SetFormatter(&logrus.JSONFormatter{}) logrus.AddHook(&AlertHook{}) // Reusable Entry: bind common fields once, log many times. req := logrus.WithFields(logrus.Fields{ "request_id": "req_abc123", "path": "/api/widgets", }) req.Info("handling request") req.WithError(errors.New("timeout")).Error("downstream call failed") } Use it when: you're working in a codebase that already uses it. Don't rip it out for vibes. Skip it when: you're greenfielding in 2026. Pick Zap or Zerolog and move on. / logrus Logrus Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. Logrus is in maintenance mode. The project focuses on security, bug fixes and performance improvements. New features are not planned, aside from changes required to provide interoperability with other logging ecosystems (e.g., Go's log/slog). I believe Logrus' biggest contribution is to have played a part in today's widespread use of structured logging in Golang. There doesn't seem to be a reason to do a major, breaking iteration into Logrus V2, since the fantastic Go community has built those independently. Many fantastic alternatives have sprung up. Logrus would look like those, had it been re-designed with what we know about structured logging in Go today. Check out, for example Zerolog, Zap, and Apex. Nicely color-coded in development (when a TTY is attached, otherwise just plain text): With logrus.SetFormatter(&logrus.JSONFormatter{}), for… View on GitHub logr is not a logger. logr is an interface for loggers. You hand a logr.Logger around your codebase, and somewhere at the top of main() you wire it to a real backend (Zap, Zerolog, whatever). Same energy as "I don't have a favorite music genre, I have a Spotify." Signature features: swappable backends (this example flips between zapr and funcr via an env var), V() verbosity levels (numeric, inverted from the usual — higher N = more verbose), and WithName/WithValues for hierarchical, fields-bound sub-loggers. package main import ( "errors" "fmt" "log" "os" "github.com/go-logr/logr" "github.com/go-logr/logr/funcr" "github.com/go-logr/zapr" "go.uber.org/zap" ) type service struct { log logr.Logger } func (s *service) handle(id string) { s.log.Info("handling", "id", id) s.log.V(1).Info("verbose: extra detail", "id", id) s.log.V(2).Info("trace: noisy debug", "id", id) s.log.Error(errors.New("database busy"), "handle failed", "id", id) } // The point of logr: this code doesn't know or care which logger backs it. func main() { var logger logr.Logger switch os.Getenv("LOGR_BACKEND") { case "funcr": logger = funcr.New(func(prefix, args string) { fmt.Println(prefix, args) }, funcr.Options{Verbosity: 1}) default: zapLog, err := zap.NewProduction() if err != nil { log.Fatal(err) } defer zapLog.Sync() logger = zapr.NewLogger(zapLog) } svc := &service{log: logger.WithName("widgets").WithValues("tenant", "acme")} svc.handle("w_1") } This is the pattern in Kubernetes and most of the cloud-native ecosystem — if you've ever touched controller-runtime you've already used it without knowing. The Go team eventually noticed and built log/slog into the standard library, borrowing heavily from logr but doing some things differently (numeric levels are inverted back, no WithName, etc.). If you're starting fresh today, slog is probably the move, but logr is still everywhere in the K8s world. Use it when: you're writing a library and don't want to force a logger on your users. Skip it when: you're writing an app. Just pick a real logger. / logr A minimal logging API for Go logr offers an(other) opinion on how Go programs and libraries can do logging without becoming coupled to a particular logging implementation. This is not an implementation of logging - it is an API. In fact it is two APIs with two different sets of users. The Logger type is intended for application and library authors. It provides a relatively small API which can be used everywhere you want to emit logs. It defers the actual act of writing logs (to files, to stdout, or whatever) to the LogSink interface. The LogSink interface is intended for logging library implementers. It is a pure interface which can be implemented by logging frameworks to provide the actual logging functionality. This decoupling allows application and library developers to write code in terms of logr.Logger (which has very low dependency fan-out) while the implementation of logging is managed "up… View on GitHub Signature feature: multi-backend logging with per-module level filtering and a built-in memory backend that's genuinely useful for tests (capture the last N records and assert on them). Here's the three-backend setup — colored stderr for humans, plain file for archive, in-memory for inspection: package main import ( "fmt" "os" "github.com/op/go-logging" ) var ( apiLog = logging.MustGetLogger("api") dbLog = logging.MustGetLogger("db") ) func main() { // Backend 1: colorized stderr. stderr := logging.NewLogBackend(os.Stderr, "", 0) stderrFmt := logging.NewBackendFormatter(stderr, logging.MustStringFormatter( `%{color}%{time:15:04:05.000} %{module} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, )) stderrLeveled := logging.AddModuleLevel(stderrFmt) stderrLeveled.SetLevel(logging.INFO, "") // default for all modules stderrLeveled.SetLevel(logging.DEBUG, "db") // chattier for the db module // Backend 2: plain file output. f, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664) defer f.Close() fileFmt := logging.NewBackendFormatter( logging.NewLogBackend(f, "", 0), logging.MustStringFormatter(`%{time:2006-01-02T15:04:05Z07:00} %{module} %{level:.4s} %{message}`), ) // Backend 3: memory backend — keeps the last N records. Great for tests. mem := logging.NewMemoryBackend(8) logging.SetBackend(stderrLeveled, fileFmt, mem) apiLog.Info("server started") dbLog.Debug("query: SELECT * FROM users") // only visible because db is DEBUG apiLog.Critical("out of disk") // Inspect the in-memory backend — assert on this in tests. for node := mem.Head(); node != nil; node = node.Next() { r := node.Record fmt.Fprintf(os.Stderr, "[mem] %s/%s: %s\n", r.Module, r.Level, r.Message()) } } The README also notes that backwards-compatibility promises were dropped on master — pin a version or use gopkg.in/op/go-logging.v1. Also: no commits in a long time. Use it when: you genuinely need pretty colored console output, per-module level filtering, and a memory backend for testing — all without extra dependencies. / go-logging Golang logging library Package logging implements a logging infrastructure for Go. Its output format is customizable and supports different logging backends like syslog, file and memory. Multiple backends can be utilized with different log levels per backend and logger. NOTE: backwards compatibility promise have been dropped for master. Please vendor this package or use gopkg.in/op/go-logging.v1 for previous version. See changelog for details. Example Let's have a look at an example which demonstrates most of the features found in this library. package main import ( "os" "github.com/op/go-logging" ) var log = logging.MustGetLogger("example") // Example format string. Everything except the message has a custom color // which is dependent on the log level. Many fields have a custom output // formatting too, eg. the time returns the hour down to the milli second. var format = logging.MustStringFormatter( `%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, … View on GitHub The honest truth: any structured logger is 1000× better than fmt.Println, and the difference between Zap and Zerolog at your actual workload is probably noise. Pick one, commit, move on, ship the feature. Now go delete those fmt.Println("here")s. What's your go-to Go logger? Roast my picks in the comments. ⬇️ AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.* Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use. ⭐ Star it on GitHub: / git-lrc | 🇩🇰 Dansk | 🇪🇸 Español | 🇮🇷 Farsi | 🇫🇮 Suomi | 🇯🇵 日本語 | 🇳🇴 Norsk | 🇵🇹 Português | 🇷🇺 Русский | 🇦🇱 Shqip | 🇨🇳 中文 | git-lrc Free, Micro AI Code Reviews That Run on Commit AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free. See It In Action See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements git-lrc-intro-60s.mp4 Why 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production. 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong. 🔁 Build a… View on GitHub
