Go slog logging
Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-slog-logging
Agent skill plugins for Go code quality and performance work.
npx -y skills add ctoth/golang-skills-plugin --skill go-slog-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
- 0 stars0 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
Guides Go logging with log/slog (Go 1.21+) — the stdlib STRUCTURED logger — over fmt.Println / log.Printf debugging and ad-hoc string logs. Prefer typed attrs (slog.String/Int) or LogAttrs to the key-value variadic form, which silently emits !BADKEY on an odd argument count; set a minimum level and a TextHandler/JSONHandler at the edges via SetDefault; attach context with With/Group and InfoContext; don't log-and-return the same error, don't log secrets/PII, and don't log.Fatal in a library (it skips defers). Auto-invokes when writing or editing logging, log/slog, slog.Info/Error, log.Printf/Println, or structured log attrs, and on "how should this log" / "add logging here" / replacing fmt.Println debugging. Logs are read by the on-call engineer at 3am.
SKILL.md
13.1 KB, as published. Nobody here has run it
Go slog Logging
"We divided the API into a frontend,
Logger, that calls a backend interface,Handler. That way, existing logging packages can talk to a common backend, so the packages that use them can interoperate without having to be rewritten." — Structured Logging with slog
log/slog (Go 1.21) is the standard library's structured logger: every line is a message plus typed key-value attributes a machine can parse, not a sentence a human has to grep. The reader of a log is the on-call engineer paged at 3am — a structured record with the right attrs is debuggable under load; a bare log.Println(err) is not. Prefer slog over fmt.Println/log.Printf debugging and over hand-concatenated string logs. This skill owns the log side of go-error-handling's "handle each error once."
1. slog Over fmt.Println and log.Printf
fmt.Println(err) and log.Printf("got %v", x) produce a line of prose: no level, no timestamp structure, no queryable fields. slog produces a record that a log pipeline can filter, index, and alert on.
// WRONG — unstructured; "user 42 failed: timeout" can't be filtered by user or cause
log.Printf("user %d request failed: %v", id, err)
fmt.Println("got here", x) // debug print left in
// RIGHT — message + typed attrs; queryable by user, status, and error
slog.Error("request failed", slog.Int("user", id), slog.Any("err", err))
A TextHandler "writes Records to an io.Writer as a sequence of key=value pairs"; a JSONHandler "writes Records ... as line-delimited JSON objects" (pkg.go.dev/log/slog). The same call serves a human in dev and an ingestion pipeline in prod — you only swap the handler.
2. Typed Attrs Over the Key-Value Variadic — the !BADKEY Footgun
slog.Info("msg", "key", value, ...) takes alternating keys and values. The convenience is real, but the form is a footgun: drop one argument and slog does not error. "If an argument is a string and this is not the last argument, the following argument is treated as the value ... Otherwise, the argument is treated as a value with key !BADKEY" (pkg.go.dev/log/slog). An odd argument count silently mislabels your data.
// WRONG — odd args: "count" has no value, so it becomes a VALUE under "!BADKEY".
// Verified output: {"level":"INFO","msg":"oops","user":"alice","!BADKEY":"count"}
slog.Info("oops", "user", "alice", "count")
// RIGHT — typed attrs: one constructor per pair, impossible to misalign
slog.Info("ok", slog.String("user", "alice"), slog.Int("count", 3))
The designers "felt strongly that this was a bad idea ... easy to get wrong by omitting a key or value" but kept the light syntax, so they "added a vet check to catch common mistakes, but did not change the design" (Go blog: slog). go vet flags a literal odd-args call — keep go vet in CI (owned by go-tooling-and-static-analysis). Typed constructors (slog.String, slog.Int, slog.Bool, slog.Time, slog.Any) sidestep the trap entirely and self-document each field's type.
3. LogAttrs for Hot Paths
For a frequently executed log statement, the typed form has an even faster sibling. LogAttrs "is similar to Logger.Log but accepts only Attrs, not alternating keys and values; this allows it, too, to avoid allocation" (pkg.go.dev/log/slog). It takes a context.Context and an explicit level:
// RIGHT — allocation-free hot path; same output as the typed Info call
slog.LogAttrs(ctx, slog.LevelInfo, "scan complete",
slog.String("table", name), slog.Int("rows", n))
The blog found "the greatest gains came from paying careful attention to memory allocation" — LogAttrs is where that pays off. Don't reach for it everywhere; use it on the genuinely hot statements where a benchmark shows allocation matters.
4. Set a Level — and Filter Cheaply
slog has four named levels — Debug, Info, Warn, Error — and "levels are just integers, so you aren't limited to the four named levels" (Go blog: slog). The handler's minimum level drops everything below it: "Level reports the minimum record level that will be logged. The handler discards records with lower levels. If Level is nil, the handler assumes LevelInfo" (pkg.go.dev/log/slog). So by default Debug is invisible — that is correct; turn it up deliberately.
// RIGHT — minimum level set on the handler; Debug records are dropped
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})
// RIGHT — LevelVar to flip verbosity at runtime without rebuilding the handler
var lvl slog.LevelVar // defaults to Info
h = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &lvl})
lvl.Set(slog.LevelDebug) // now Debug appears
A dropped record is cheap: the handler's Enabled check runs "at the beginning of every log event, giving the handler a chance to drop unwanted log events quickly" (Go blog: slog). HandlerOptions also offers AddSource (adds the call site) and ReplaceAttr (rewrite/redact each attr before output).
5. Pick a Handler and Set the Default at the Edges
Configure logging once, at the program edge (main/startup): build a handler, wrap it in a *slog.Logger, and install it with slog.SetDefault. "SetDefault makes l the default Logger, which is used by the top-level functions Info, Debug and so on" (pkg.go.dev/log/slog). After that the rest of the program calls slog.Info/slog.Error or is handed a *slog.Logger — it never reconfigures global state.
func main() {
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})
slog.SetDefault(slog.New(h)) // JSON for prod ingestion; TextHandler for dev
// ... run the app; deeper code just calls slog.Info(...) ...
}
TextHandler (key=value, readable) for local development; JSONHandler (line-delimited JSON) for production ingestion. Choosing the handler is a one-line edge decision — don't sprinkle handler construction through the codebase (the global-config-everywhere anti-pattern; go-idiomatic-discipline bans mutable global reconfiguration).
6. With for Persistent Context, Group for Nesting
When several log calls share an attribute (a request ID, a user), don't repeat it. With "returns a Logger that includes the given attributes in each output operation" (pkg.go.dev/log/slog) — and pre-formats them once, so it is faster as well as terser.
// RIGHT — requestID rides every record from reqLog, set once
reqLog := slog.Default().With(slog.String("requestID", id))
reqLog.Info("received") // ...,"requestID":"abc-123"
reqLog.Info("validated") // ...,"requestID":"abc-123"
// RIGHT — Group nests related fields under one key
slog.Info("connected", slog.Group("db",
slog.String("host", host), slog.Int("port", port)))
// ...,"db":{"host":"db1","port":5432}
7. Carry the Context — InfoContext
Info/Warn/Error have ...Context variants (InfoContext, ErrorContext, and the general Log/LogAttrs) that take a context.Context first: "InfoContext logs at LevelInfo with the given context" (pkg.go.dev/log/slog). Pass ctx so a handler can extract request-scoped values — trace IDs, deadlines — and so cancellation is observable.
// RIGHT — ctx flows to the handler, which can attach a trace ID from it
slog.InfoContext(ctx, "handling", slog.String("route", r.URL.Path))
go-context owns context propagation and trace plumbing; the rule here is simply: in request-scoped code, prefer the ...Context form.
8. Don't Log-and-Return the Same Error
The highest-frequency logging mistake is double handling: logging an error and returning it, so every layer logs the same failure and the log fills with duplicates of one incident. An error is handled once — match it, recover from it, or return it (wrapped), but not "log it and return it."
// WRONG — logged here AND returned; every caller logs it again
if err != nil {
slog.Error("get user failed", slog.Any("err", err))
return err
}
// RIGHT — wrap and return; the top-level handler logs it once
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
Logging and degrading gracefully (not returning) is fine — that is handling it once. The wrap-and-return mechanics are owned by go-error-handling; this skill owns the log side of its "handle once."
9. Never Log Secrets, Tokens, or PII
A log is a durable, widely-replicated, often-third-party-ingested artifact. Anything you log may sit in a SIEM, a vendor dashboard, and a backup for years. Passwords, API keys, session tokens, full card numbers, and personal data must never reach an attr.
// WRONG — token and password now live in every log sink forever
slog.Info("login", slog.String("token", tok), slog.String("password", pw))
// RIGHT — log an identifier and an outcome, never the credential
slog.Info("login ok", slog.String("user", user), slog.Bool("mfa", true))
Redact centrally with HandlerOptions.ReplaceAttr, or implement slog.LogValuer on a secret type so it renders as REDACTED. The default is: log the fact, not the secret.
10. Don't log.Fatal / panic for Ordinary Errors in a Library
log.Fatal "is equivalent to Print followed by a call to os.Exit(1)" (pkg.go.dev/log) — and os.Exit runs no deferred functions, so every defer (flushes, unlocks, cleanup) is skipped and the process dies mid-flight. A library has no business killing its caller's process over a failed lookup. log.Panic "is equivalent to Print followed by a call to panic()."
// WRONG — in a library: kills the caller's process, skips all defers
func Load(path string) Config {
data, err := os.ReadFile(path)
if err != nil {
log.Fatal(err) // os.Exit(1): no defer runs, caller can't recover
}
// ...
}
// RIGHT — return the error; let the caller (or main) decide to exit
func Load(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("read config %s: %w", path, err)
}
// ...
}
log.Fatal is acceptable only at the very top of main, where exiting is the intended outcome. go-defer-panic-recover owns why os.Exit skips defers; go-error-handling owns returning errors as values.
11. Don't Over-Log in Hot Loops
A log inside a tight loop can dominate runtime and bury the signal. Log state transitions and failures, not every iteration; gate detail behind Debug (dropped cheaply at Info) or aggregate into one summary.
// WRONG — one record per row; millions of lines, the real event is invisible
for _, row := range rows {
slog.Info("processing row", slog.Int("id", row.ID))
}
// RIGHT — Debug (filtered out in prod) for the detail; Info for the summary
for _, row := range rows {
slog.Debug("processing row", slog.Int("id", row.ID))
}
slog.Info("batch done", slog.Int("rows", len(rows)))
12. Routing to Related Skills
go-idiomatic-discipline— the policy root; "errors are values, never silently discarded" and no mutable global reconfiguration.go-error-handling— wrap-and-return; the don't log-and-return rule of §8 is the log side of its "handle once."go-context—InfoContextand propagating trace IDs / cancellation throughctx(§7).go-defer-panic-recover— whylog.Fatal'sos.Exitskips everydefer(§10), and whenpanicis legitimate.go-version-feature-map—log/slogarrived in Go 1.21; check the module'sgodirective before using it.go-testing-tabledriven— capturing handler output into abytes.Bufferto assert on log records in tests.go-tooling-and-static-analysis— thesloggo vetcheck that catches the literal odd-args!BADKEYbug (§2).
13. Reference Files
High-frequency slog anti-patterns in LLM-generated Go, each with wrong/right code and citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml