I have finally torn the standard library log package out of a service and replaced it with proper structured logging, and I am annoyed it took me this long. The problem with log.Printf("user %s failed login from %s", user, ip) is not that it is wrong. It is that the moment you want to ask "show me every failed login for this user across the fleet", you are reduced to grep and regular expressions against a sentence you wrote by hand.
Structured logging fixes this by treating a log line as a set of fields rather than a string. With zap, the same event looks like:
logger.Info("login failed",
zap.String("user", user),
zap.String("ip", ip),
zap.Int("attempt", n),
)
That comes out as JSON, one object per line, with user and ip as real keys. Now the question is a query, not a regex: filter on user, count by ip, group by whatever you logged. The log aggregator does the work it was built to do, and I stop writing fragile patterns to claw structure back out of prose I threw away on the way in.
The other quiet win is the contextual logger. Attach a request ID once, derive a child logger that carries it, and every line for that request is automatically tagged. Tracing one request through a noisy service stops being archaeology. I chose zap mostly for speed, though zerolog would have done the same job. The library matters less than the shift in habit: log fields, not sentences, and let the machine do the searching.