← GophersCONTENT HISTORY

Update to Gophers

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "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.",
  "included_files": [
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 215
    },
    {
      "relative_path": "references/levels-and-context.md",
      "size_in_bytes": 3990
    },
    {
      "relative_path": "references/request-scope-and-middleware.md",
      "size_in_bytes": 4179
    },
    {
      "relative_path": "references/slog-handler-ecosystem.md",
      "size_in_bytes": 5476
    }
  ],
  "name": "go-logging",
  "skill_md_contents": "---\nname: go-logging\ndescription: \"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.\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Requires Go 1.21+ for log/slog. Go 1.26 slog.NewMultiHandler is noted where relevant.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go Logging\n\nLogs 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.\n\n> This skill covers **logging only**. Metrics, distributed tracing, profiling, and RUM are a separate concern — they belong to a future `go-observability` skill. Do not confuse them with logging here.\n\n## Core Rules\n\n1. **Use `log/slog`** for new code. Structured, leveled, in the standard library since Go 1.21.\n2. **Static message, structured fields.** The message describes what happened; data goes in key-value attributes.\n3. **Log or return, never both.** Logging a wrapped error makes the same failure appear at every layer.\n4. **Log at the boundary.** HTTP handlers, job runners, and `main` log. Library code wraps and returns.\n5. **Use snake_case keys** consistently across the codebase (`user_id`, `request_id`, `elapsed_ms`).\n6. **`slog.Error` always carries an `\"err\"` attribute.** Without it, you logged a sentence, not an error.\n7. **Never log secrets, PII, or unbounded data.** Tokens, full credit cards, request bodies — none of it.\n\n## Choosing a Logger\n\n| Situation | Use |\n|---|---|\n| New production service | `log/slog` |\n| Trivial CLI / one-off script | `log` (the standard package) |\n| Measured hot-path bottleneck where slog dominates the flame graph | `zap` or `zerolog`, but keep the structured style |\n| Existing zap/logrus/zerolog code | Migrate to `slog` with a bridge handler; see [references/slog-handler-ecosystem.md](references/slog-handler-ecosystem.md) |\n\n`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.\n\n## Structured Logging\n\nBuild log messages from a **static message** plus typed fields:\n\n```go\n// Good — static message, structured fields\nslog.Info(\"order placed\", \"order_id\", orderID, \"total_cents\", totalCents)\n\n// Bad — dynamic data baked into the message string\nslog.Info(fmt.Sprintf(\"order %d placed for $%.2f\", orderID, total))\n```\n\nThe aggregator (Loki, Elastic, CloudWatch) can index `order_id`. It cannot index a sprintf'd sentence.\n\nFor hot paths, typed constructors avoid allocations:\n\n```go\nslog.LogAttrs(ctx, slog.LevelInfo, \"request handled\",\n    slog.String(\"method\", r.Method),\n    slog.Int(\"status\", code),\n    slog.Duration(\"elapsed\", elapsed),\n)\n```\n\n## Log Levels\n\n| Level | When | Default |\n|---|---|---|\n| `Debug` | Developer-only diagnostics; tracing internal state | Disabled in prod |\n| `Info` | Notable lifecycle events: startup, shutdown, config loaded | Enabled |\n| `Warn` | Unexpected but recoverable: retry succeeded, deprecated flag used | Enabled |\n| `Error` | Operation failed; someone should look | Enabled |\n\nRules of thumb:\n\n- If nobody should act on it, it is not `Error` — use `Warn` or `Info`.\n- If it is only useful with a debugger attached, it is `Debug`.\n- `slog.Error` must include an `\"err\"` attribute.\n\n```go\nslog.Error(\"payment failed\", \"err\", err, \"order_id\", id)\nslog.Warn(\"retry succeeded\", \"attempt\", n, \"endpoint\", url)\nslog.Info(\"server started\", \"addr\", addr)\nslog.Debug(\"cache lookup\", \"key\", key, \"hit\", hit)\n```\n\n> Read [references/levels-and-context.md](references/levels-and-context.md) when choosing between `Warn` and `Error`, defining custom verbosity levels, or pre-checking `Enabled()` on hot paths.\n\n## Request-Scoped Logging\n\nDerive a logger per request that carries the fields every downstream call should include:\n\n```go\nfunc middleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        log := slog.With(\"request_id\", requestID(r))\n        ctx := context.WithValue(r.Context(), loggerKey{}, log)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\nfunc FromContext(ctx context.Context) *slog.Logger {\n    if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l }\n    return slog.Default()\n}\n```\n\nUse the `Context`-aware variants (`slog.InfoContext`, `slog.ErrorContext`) so handlers that read trace IDs from the context can stamp them into the record:\n\n```go\nslog.InfoContext(ctx, \"order placed\", \"order_id\", id)\n```\n\n> Read [references/request-scope-and-middleware.md](references/request-scope-and-middleware.md) when wiring request IDs, building logging middleware, or choosing between context-stored loggers and explicit parameters.\n\n## Log or Return — Not Both\n\nLogging an error and then returning it produces the same failure at every layer, and three log records for one bug:\n\n```go\n// Bad — every caller up the stack logs it again\nif err != nil {\n    slog.Error(\"query failed\", \"err\", err)\n    return fmt.Errorf(\"query: %w\", err)\n}\n\n// Good — wrap and return; the boundary logs once\nif err != nil {\n    return fmt.Errorf(\"loading user %d: %w\", id, err)\n}\n```\n\nThe **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:\n\n```go\nif err := checkout(ctx); err != nil {\n    slog.ErrorContext(ctx, \"checkout failed\", \"err\", err, \"user_id\", uid)\n    http.Error(w, \"internal error\", http.StatusInternalServerError)\n    return\n}\n```\n\nSee the `go-error-handling` skill for the full handle-once pattern.\n\n## What Not to Log\n\n- Passwords, API keys, tokens, session IDs.\n- Full credit card numbers, SSNs, government IDs.\n- Request or response bodies that may contain user data.\n- Whole slices or maps of unbounded size (log lengths instead).\n- Anything you would not want appearing in a customer support screenshot.\n\nUse 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](references/slog-handler-ecosystem.md).\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| `log.Printf(\"msg %v\", v)` | Unstructured; impossible to index | `slog.Info(\"msg\", \"key\", v)` |\n| `fmt.Sprintf` inside the message | Data is now part of the string | Static message + key/value attrs |\n| Logging and returning the same error | Duplicate log records, noisy alerts | Wrap and return; log at the boundary |\n| `slog.Info(\"err: %v\", err)` | Drops level semantics and structure | `slog.Error(\"op failed\", \"err\", err)` |\n| New logger per call | Loses request-scoped fields | Derive once in middleware, pass via context |\n| Mixed key styles (`userId`, `user_id`, `UserID`) | Aggregators index them as different fields | Pick `snake_case` and stick to it |\n| Logging the whole request body | Leaks PII; explodes log volume | Log lengths and content type only |\n| Introducing zap/zerolog without a benchmark | Extra dependency for no measured win | Stay on `slog`; benchmark before switching |\n\n## Verification Checklist\n\nBefore finishing a logging change:\n\n- [ ] All new log calls use `log/slog`, not `log.Printf`\n- [ ] Each call has a static message and key-value attributes\n- [ ] `slog.Error` calls carry an `\"err\"` attribute\n- [ ] No call both logs and returns the same error\n- [ ] Keys use `snake_case` and match existing keys in the codebase\n- [ ] Handlers use `*Context` variants so trace correlation works\n- [ ] No secrets, PII, or unbounded values appear in attribute values\n- [ ] Request-scoped fields are added in middleware, not at each call site\n\n## References\n\n- [references/levels-and-context.md](references/levels-and-context.md) — picking levels, `Enabled()` gating, custom verbosity\n- [references/request-scope-and-middleware.md](references/request-scope-and-middleware.md) — request IDs, context-stored loggers, HTTP middleware\n- [references/slog-handler-ecosystem.md](references/slog-handler-ecosystem.md) — JSON/text handlers, multi-handler, bridges from zap/logrus/zerolog, redaction\n"
}

SHA-256 of public snapshot: b5ac2be48636afe42bf8d8ab22a03993353990756ac59fc4461eb387d55f2267