Logging & Observability
Colored logs in dev, structured logs for tools, errors to Sentry. all from .env
Built on Go’s native slog. Colored and readable while you build, structured when a tool needs it, errors shipped to Sentry. Three env vars, zero code changes.
Three knobs
LOG_LEVEL= # debug | info | warn | error (empty = debug in dev, info in prod)
LOG_FORMAT=pretty # pretty (colored, for humans) | json (for log tools)
LOG_HTTP=true # HTTP request logging on/off
That’s the whole API. Everything below is just explaining when to reach for each.
LOG_LEVEL: how loud?
Decoupled from APP_ENV, so you can crank debug on a live server while chasing a bug. no redeploy.
LOG_LEVEL=debug # see everything, including slog.Debug lines
LOG_LEVEL=warn # only warnings and errors
Log chatty internals at Debug and they vanish at info, reappear on demand:
slog.Debug("cache miss", "key", key) // hidden unless LOG_LEVEL=debug
LOG_FORMAT: for your eyes or for a tool?
pretty 08:27:37.781 CEST INF http request method=GET path=/docs status=200 duration_ms=29
json {"time":"...","level":"INFO","msg":"http request","method":"GET","path":"/docs","status":200}
pretty(default): colored, aligned, human-readable. Colors drop automatically when stdout isn’t a terminal, so log files stay clean.json: switch to this the moment a log aggregator scrapes your stdout (Loki, Datadog, Papertrail) and needs to parse fields likestatus>=500.
Reading fly logs / Railway with your eyes? Stay on pretty. Piping stdout into a tool? Flip to json. One line.
LOG_HTTP: request logs on or off?
Every request is logged with method, path, status, and duration. Static assets (/assets, /templui, /uploads, favicon) are always skipped so they don’t bury the real traffic.
Got a reverse proxy (Caddy, nginx, Traefik) that already logs requests? Set LOG_HTTP=false to kill the duplicate.
Errors to Sentry (optional)
SENTRY_DSN=https://[email protected]/xxx
Sentry is not affected by LOG_FORMAT. It runs as its own handler and receives only slog.Error calls with full stack traces, grouping, and alerts. So you get colored pretty console and Sentry at the same time, in dev or prod. Leave the DSN empty to disable.
Free tier: 5,000 errors/month. Grab a DSN at sentry.io.
Why no Loki/Datadog handler?
Those tools ingest your stdout (via the platform or an agent). that’s the standard, 12-factor way. So the lever for them is LOG_FORMAT=json, not a built-in push handler you’d have to configure and maintain. Sentry is different: error tracking is a push SDK by nature, so it gets its own handler.
Need direct push anyway? The logger fans out handlers via samber/slog-multi, so adding slog-loki or slog-datadog is ~5 lines in internal/logger/logger.go. The door’s open, it’s just not in your way.
Usage
slog.Info("user logged in", "user_id", user.ID, "email", user.Email)
slog.Warn("rate limit near", "ip", ip, "count", n)
slog.Error("failed to connect", "error", err, "host", dbHost) // also goes to Sentry
Where it’s wired
One call in cmd/server/main.go, driven by config:
logger.Init(logger.Options{
Level: cfg.LogLevel, // LOG_LEVEL
Format: cfg.LogFormat, // LOG_FORMAT
IsDev: cfg.IsDevelopment(),
SentryDSN: cfg.SentryDSN, // SENTRY_DSN
})
Colored output uses lmittmann/tint. Everything else is standard-library slog.