Logging
| The log package | |
| Structured slog | |
| Levels | |
| Rotate by design (write to stderr, rotate outside) |
The log package
The log package prints timestamped lines with zero setup. Each call writes to standard error by default, keeping output pipes clean. Flags add dates, times, and file positions to every line. Small services and CLIs can stop right here.
package main
import (
"log"
"os"
)
func main() {
log.SetOutput(os.Stderr)
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("server starting")
log.Println("listening on :8080")
}
SetOutput makes the stderr target explicit for readers of your code. Lshortfile adds the file and line of each call, which speeds up debugging. Fatal and Panic variants exist but end the program, so reach for them sparingly.
Structured slog
log/slog logs key and value pairs instead of flat text. Machines parse the JSON form easily, and humans read the text form. Attributes travel with each event, so port numbers and IDs never get lost. New projects should start here rather than with plain log.
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
logger.Info("server starting", "port", 8080)
logger.Error("demo failure", "err", "connection refused")
}
{"time":"...","level":"INFO","msg":"server starting","port":8080}
{"time":"...","level":"ERROR","msg":"demo failure","err":"connection refused"}
Keys stay strings by convention, while values can be numbers, strings, or errors. JSON output suits collectors and dashboards. Swap in NewTextHandler for readable local development.
Levels
Levels rank events from noisy Debug up to urgent Error. The handler drops everything below its minimum, so one setting controls volume. Development runs at Debug while production sits at Info or higher. Choosing the level is part of writing the log call.
package main
import (
"log/slog"
"os"
)
func main() {
opts := &slog.HandlerOptions{Level: slog.LevelDebug}
logger := slog.New(slog.NewTextHandler(os.Stderr, opts))
logger.Debug("cache miss", "key", "user:7")
logger.Info("request done", "ms", 12)
logger.Warn("retrying", "attempt", 2)
logger.Error("disk full", "path", "/var/data")
}
Debug traces internals, Info marks normal milestones, Warn flags trouble ahead, and Error reports failure. Raising the minimum to Info silences Debug in production. The level lives in config next to the port number.
Rotate by design (write to stderr, rotate outside)
Programs should write to stderr and never manage files themselves. Rotation, retention, and shipping belong to the platform around the app. This split keeps code small and operations flexible. The shell redirect below stands in for that platform.
package main
import "log/slog"
func main() {
slog.Info("server starting", "port", 8080)
slog.Info("request done", "ms", 12)
}
go run main.go 2>> app.log
time=... level=INFO msg="server starting" port=8080
time=... level=INFO msg="request done" ms=12
The default slog logger already targets stderr, so no setup is needed. Appending with 2>> collects the stream into a file. Outside tools then rotate that file on size or schedule without touching Go code.
Next: Regular expressions
Article author: Arthur Isaev