agentsclimarketplace

Go slog logging

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-slog-logging

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.From its SKILL.md

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-slog-logging

Assembled 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.

SKILL.md

13.1 KB, ~3.2k tokens by cl100k_base, 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-contextInfoContext and propagating trace IDs / cancellation through ctx (§7).
  • go-defer-panic-recover — why log.Fatal's os.Exit skips every defer (§10), and when panic is legitimate.
  • go-version-feature-maplog/slog arrived in Go 1.21; check the module's go directive before using it.
  • go-testing-tabledriven — capturing handler output into a bytes.Buffer to assert on log records in tests.
  • go-tooling-and-static-analysis — the slog go vet check that catches the literal odd-args !BADKEY bug (§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

What ships with it: 2 files

13.0 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.