go-logging

$npx mdskill add cxuu/golang-skills/go-logging

Configure and use structured logging in Go with slog.

  • Helps choose a logging approach and configure slog for production.
  • Depends on Go's log/slog package and related references.
  • Recommends slog by default, third-party only if benchmarked.
  • Provides patterns for handlers, context, testing, and migration.

SKILL.md

.github/skills/go-loggingView on GitHub ↗
---
name: go-logging
description: Use when choosing a logging approach, configuring slog, writing structured log statements, or deciding log levels in Go. Also use when setting up production logging, adding request-scoped context to logs, or migrating from log to slog, even if the user doesn't explicitly mention logging. Does not cover error handling strategy (see go-error-handling).
---

# Go Logging

> Compatibility: `log/slog` requires Go 1.21+; `testing/slogtest` requires Go 1.22+.

## Resource Routing

- `references/LEVELS-AND-CONTEXT.md` - Read when choosing log levels, deciding logger-in-context versus explicit parameters, or excluding sensitive fields.
- `references/LOGGING-PATTERNS.md` - Read when configuring slog handlers, logging HTTP requests, testing handlers, or migrating from `log.Printf`.

## Core Principle

Logs are for **operators**, not developers. Every log line should help someone
diagnose a production issue. If it doesn't serve that purpose, it's noise.

---

## Choosing a Logger

> **Normative**: Use `log/slog` for new Go code.

`slog` is structured, leveled, and in the standard library (Go 1.21+). It
covers the vast majority of production logging needs.

```
Which logger?
├─ New production code      → log/slog
├─ Trivial CLI / one-off    → log (standard)
└─ Measured perf bottleneck → zerolog or zap (benchmark first)
```

Do not introduce a third-party logging library unless profiling shows `slog`
is a bottleneck in your hot path. When you do, keep the same structured
key-value style.

---

## Structured Logging

> **Normative**: Always use key-value pairs. Never interpolate values into the message string.

The message is a **static description** of what happened. Dynamic data goes in
key-value attributes:

```go
// Good: static message, structured fields
slog.Info("order placed", "order_id", orderID, "total", total)

// Bad: dynamic data baked into the message string
slog.Info(fmt.Sprintf("order %d placed for $%.2f", orderID, total))
```

### Key Naming

> **Advisory**: Use `snake_case` for log attribute keys.

Keys should be lowercase, underscore-separated, and consistent across the
codebase: `user_id`, `request_id`, `elapsed_ms`.

### Typed Attributes

For performance-critical paths, use typed constructors to avoid allocations:

```go
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
    slog.String("method", r.Method),
    slog.Int("status", code),
    slog.Duration("elapsed", elapsed),
)
```

---

## Log Levels

> **Advisory**: Follow these level semantics consistently.

| Level | When to use | Production default |
|-------|-------------|--------------------|
| Debug | Developer-only diagnostics, tracing internal state | Disabled |
| Info  | Notable lifecycle events: startup, shutdown, config loaded | Enabled |
| Warn  | Unexpected but recoverable: deprecated feature used, retry succeeded | Enabled |
| Error | Operation failed, requires operator attention | Enabled |

**Rules of thumb**:
- If nobody should act on it, it's not Error — use Warn or Info
- If it's only useful with a debugger attached, it's Debug
- `slog.Error` should always include an `"err"` attribute

```go
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)
```

---

## Request-Scoped Logging

> **Advisory**: Derive loggers from context to carry request-scoped fields.

Use middleware to enrich a logger with request ID, user ID, or trace ID, then
pass the enriched logger downstream via context or as an explicit parameter.
Keep the full context-key and middleware implementation in the logging patterns
reference so request-scoped logging has one owner.

---

## Log or Return, Not Both

The handle-once rule belongs to [go-error-handling](../go-error-handling/SKILL.md).
In logging work, apply it by choosing either a local log-and-recover path or a
return path with context, not both for the same error.

**Exception**: HTTP handlers and other top-of-stack boundaries may log detailed
errors server-side while returning a sanitized message to the client:

```go
if err != nil {
    slog.Error("checkout failed", "err", err, "user_id", uid)
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}
```

See [go-error-handling](../go-error-handling/SKILL.md) for the full
handle-once pattern and error wrapping guidance.

---

## What NOT to Log

> **Normative**: Never log secrets, credentials, PII, or high-cardinality unbounded data.

- Passwords, API keys, tokens, session IDs
- Full credit card numbers, SSNs
- Request/response bodies that may contain user data
- Entire slices or maps of unbounded size

---

## Quick Reference

| Do | Don't |
|----|-------|
| `slog.Info("msg", "key", val)` | `log.Printf("msg %v", val)` |
| Static message + structured fields | `fmt.Sprintf` in message |
| `snake_case` keys | camelCase or inconsistent keys |
| Log OR return errors | Log AND return the same error |
| Derive logger from context | Create a new logger per call |
| Use `slog.Error` with `"err"` attr | `slog.Info` for errors |
| Pre-check `Enabled()` on hot paths | Always allocate log args |

---

## Related Skills

- **Error handling**: See [go-error-handling](../go-error-handling/SKILL.md) when deciding whether to log or return an error, or for the handle-once pattern
- **Context propagation**: See [go-context](../go-context/SKILL.md) when passing request-scoped values (including loggers) through context
- **Performance**: See [go-performance](../go-performance/SKILL.md) when optimizing hot-path logging or reducing allocations in log calls
- **Code review**: See [go-code-review](../go-code-review/SKILL.md) when reviewing logging practices in Go PRs

More from cxuu/golang-skills

SkillDescription
go-code-reviewUse when reviewing Go code or checking code against community style standards. Also use proactively before submitting a Go PR or when reviewing any Go code changes, even if the user doesn't explicitly request a style review. Does not cover language-specific syntax — delegates to specialized skills.
go-concurrencyUse when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
go-contextUse when working with context.Context in Go — placement in signatures, propagating cancellation and deadlines, and storing values in context vs parameters. Also use when cancelling long-running operations, setting timeouts, or passing request-scoped data, even if they don't mention context.Context directly. Does not cover goroutine lifecycle or sync primitives (see go-concurrency).
go-control-flowUse when writing conditionals, loops, or switch statements in Go — including if with initialization, early returns, for loop forms, range, switch, type switches, and blank identifier patterns. Also use when writing a simple if/else or for loop, even if the user doesn't mention guard clauses or variable scoping. Does not cover error flow patterns (see go-error-handling).
go-data-structuresUse when working with Go slices, maps, or arrays — choosing between new and make, using append, declaring empty slices (nil vs literal for JSON), implementing sets with maps, and copying data at boundaries. Also use when building or manipulating collections, even if the user doesn't ask about allocation idioms. Does not cover concurrent data structure safety (see go-concurrency).
go-declarationsUse when declaring or initializing Go variables, constants, structs, or maps — including var vs :=, reducing scope with if-init, formatting composite literals, designing iota enums, and using any instead of interface{}. Also use when writing a new struct or const block, even if the user doesn't ask about declaration style. Does not cover naming conventions (see go-naming).
go-defensiveUse when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).
go-documentationUse when writing or reviewing documentation for Go packages, types, functions, or methods. Also use proactively when creating new exported types, functions, or packages, even if the user doesn't explicitly ask about documentation. Does not cover code comments for non-exported symbols (see go-style-core).
go-error-handlingUse when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).
go-functional-optionsUse when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).