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.
1. Zap 24.5k ⭐ — The "Premature Optimization Is My Personality" Logger ⚡
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
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…
2. Zerolog 12.4k ⭐ — Zap's Cooler, Chainier Cousin 🪵
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.
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
3. Logrus 25.7k ⭐ — The Beloved Elder Statesman 👴
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 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…
4. go-logr 1.4k — The "I Don't Pick Loggers, I Pick Interfaces" Logger 🧠
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.
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…
5. go-logging 1.8k ⭐ — The One Your Senior Dev Still Defends 🪦
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.
op
/
go-logging
Golang logging library
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}`,…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:
HexmosTech
/
git-lrc
Free, Micro AI Code Reviews That Run on Commit
| 🇩🇰 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…












Top comments (0)