Go logging
26 production-grade Go skills for Claude Code, Gemini CLI, and opencode.
npx -y skills add muratmirgun/gophers --skill go-loggingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when choosing a Go logger, configuring slog, writing structured log statements, picking log levels, or attaching request-scoped fields. Apply proactively whenever code calls log/fmt to emit operational information, migrates off log/logrus/zap/zerolog, or sets up production logging. Covers structured logging only — metrics, traces, profiling, and RUM belong to a separate observability skill.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.7 KB, as published. Nobody here has run it
Go Logging
Logs are written for operators — the human who will be paged at 3 a.m. and needs to know what happened. Every log line either helps diagnose a production issue or it is noise. log/slog from the standard library is the default; reach for anything else only after measuring.
This skill covers logging only. Metrics, distributed tracing, profiling, and RUM are a separate concern — they belong to a future
go-observabilityskill. Do not confuse them with logging here.
Core Rules
- Use
log/slogfor new code. Structured, leveled, in the standard library since Go 1.21. - Static message, structured fields. The message describes what happened; data goes in key-value attributes.
- Log or return, never both. Logging a wrapped error makes the same failure appear at every layer.
- Log at the boundary. HTTP handlers, job runners, and
mainlog. Library code wraps and returns. - Use snake_case keys consistently across the codebase (
user_id,request_id,elapsed_ms). slog.Erroralways carries an"err"attribute. Without it, you logged a sentence, not an error.- Never log secrets, PII, or unbounded data. Tokens, full credit cards, request bodies — none of it.
Choosing a Logger
| Situation | Use |
|---|---|
| New production service | log/slog |
| Trivial CLI / one-off script | log (the standard package) |
| Measured hot-path bottleneck where slog dominates the flame graph | zap or zerolog, but keep the structured style |
| Existing zap/logrus/zerolog code | Migrate to slog with a bridge handler; see references/slog-handler-ecosystem.md |
slog's API is stable, the ecosystem has consolidated around it, and JSON output works with every log shipper. Do not introduce a third-party logger without a benchmark showing the win.
Structured Logging
Build log messages from a static message plus typed fields:
// Good — static message, structured fields
slog.Info("order placed", "order_id", orderID, "total_cents", totalCents)
// Bad — dynamic data baked into the message string
slog.Info(fmt.Sprintf("order %d placed for $%.2f", orderID, total))
The aggregator (Loki, Elastic, CloudWatch) can index order_id. It cannot index a sprintf'd sentence.
For hot paths, typed constructors avoid allocations:
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
slog.String("method", r.Method),
slog.Int("status", code),
slog.Duration("elapsed", elapsed),
)
Log Levels
| Level | When | Default |
|---|---|---|
Debug | Developer-only diagnostics; tracing internal state | Disabled in prod |
Info | Notable lifecycle events: startup, shutdown, config loaded | Enabled |
Warn | Unexpected but recoverable: retry succeeded, deprecated flag used | Enabled |
Error | Operation failed; someone should look | Enabled |
Rules of thumb:
- If nobody should act on it, it is not
Error— useWarnorInfo. - If it is only useful with a debugger attached, it is
Debug. slog.Errormust include an"err"attribute.
slog.Error("payment failed", "err", err, "order_id", id)
slog.Warn("retry succeeded", "attempt", n, "endpoint", url)
slog.Info("server started", "addr", addr)
slog.Debug("cache lookup", "key", key, "hit", hit)
Read references/levels-and-context.md when choosing between
WarnandError, defining custom verbosity levels, or pre-checkingEnabled()on hot paths.
Request-Scoped Logging
Derive a logger per request that carries the fields every downstream call should include:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := slog.With("request_id", requestID(r))
ctx := context.WithValue(r.Context(), loggerKey{}, log)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l }
return slog.Default()
}
Use the Context-aware variants (slog.InfoContext, slog.ErrorContext) so handlers that read trace IDs from the context can stamp them into the record:
slog.InfoContext(ctx, "order placed", "order_id", id)
Read references/request-scope-and-middleware.md when wiring request IDs, building logging middleware, or choosing between context-stored loggers and explicit parameters.
Log or Return — Not Both
Logging an error and then returning it produces the same failure at every layer, and three log records for one bug:
// Bad — every caller up the stack logs it again
if err != nil {
slog.Error("query failed", "err", err)
return fmt.Errorf("query: %w", err)
}
// Good — wrap and return; the boundary logs once
if err != nil {
return fmt.Errorf("loading user %d: %w", id, err)
}
The only layer that logs is the one that finishes the work: the HTTP handler, the job runner, main. That layer may log a detailed record server-side while returning a sanitised message to the client:
if err := checkout(ctx); err != nil {
slog.ErrorContext(ctx, "checkout failed", "err", err, "user_id", uid)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
See the go-error-handling skill for the full handle-once pattern.
What Not to Log
- Passwords, API keys, tokens, session IDs.
- Full credit card numbers, SSNs, government IDs.
- Request or response bodies that may contain user data.
- Whole slices or maps of unbounded size (log lengths instead).
- Anything you would not want appearing in a customer support screenshot.
Use a redacting slog.Handler (or wrap your own) so sensitive keys are blanked at the handler level, not at every call site. See references/slog-handler-ecosystem.md.
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
log.Printf("msg %v", v) | Unstructured; impossible to index | slog.Info("msg", "key", v) |
fmt.Sprintf inside the message | Data is now part of the string | Static message + key/value attrs |
| Logging and returning the same error | Duplicate log records, noisy alerts | Wrap and return; log at the boundary |
slog.Info("err: %v", err) | Drops level semantics and structure | slog.Error("op failed", "err", err) |
| New logger per call | Loses request-scoped fields | Derive once in middleware, pass via context |
Mixed key styles (userId, user_id, UserID) | Aggregators index them as different fields | Pick snake_case and stick to it |
| Logging the whole request body | Leaks PII; explodes log volume | Log lengths and content type only |
| Introducing zap/zerolog without a benchmark | Extra dependency for no measured win | Stay on slog; benchmark before switching |
Verification Checklist
Before finishing a logging change:
- All new log calls use
log/slog, notlog.Printf - Each call has a static message and key-value attributes
-
slog.Errorcalls carry an"err"attribute - No call both logs and returns the same error
- Keys use
snake_caseand match existing keys in the codebase - Handlers use
*Contextvariants so trace correlation works - No secrets, PII, or unbounded values appear in attribute values
- Request-scoped fields are added in middleware, not at each call site
References
- references/levels-and-context.md — picking levels,
Enabled()gating, custom verbosity - references/request-scope-and-middleware.md — request IDs, context-stored loggers, HTTP middleware
- references/slog-handler-ecosystem.md — JSON/text handlers, multi-handler, bridges from zap/logrus/zerolog, redaction