Syslog, Journal, and Ops Logging
Syslog, Journal, and Ops Logging
Overview
Systems daemons historically logged to syslog; under systemd, messages often land in the journal. Go services commonly use log/slog to stderr (captured by the supervisor) or write to syslog. Pick one clear path so operators know where to tail.
Default modern pattern: stderr + supervisor
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})
slog.SetDefault(slog.New(h))
slog.Info("started", "pid", os.Getpid())| Supervisor | Where logs go |
|---|---|
| systemd | journal (journalctl -u myapp) |
| Kubernetes | container runtime → log driver |
| Docker | docker logs |
| cron | mail / captured by cron daemon |
Prefer this for new services: no syslog dependency, works in all containers.
When to use syslog
- Policy requires
/dev/log
- Multi-process host with centralized rsyslog/syslog-ng
- Very old deployment molds
import "log/syslog"
w, err := syslog.Dial("", "", syslog.LOG_INFO|syslog.LOG_DAEMON, "myapp")
if err != nil {
// fall back to stderr
}
defer w.Close()
slog.SetDefault(slog.New(slog.NewTextHandler(io.MultiWriter(os.Stderr, w), nil)))Network syslog (Dial("udp", "logger:514", ...)) needs reliability/security thought (TLS, auth)—often better handled by a local agent.
Levels and severity
| slog | syslog-ish |
|---|---|
| Debug | DEBUG |
| Info | INFO |
| Warn | WARNING |
| Error | ERR |
Don’t log at Error for routine misses (cache miss ≠ outage).
Structured fields for ops
slog.Info("child_exit",
"cmd", name,
"code", exitCode,
"dur_ms", dur.Milliseconds(),
)Include: request/job id, host, version, signal reason.
Avoiding log loops
If your program ships logs by watching files it also writes, guard paths (don’t ingest your own output). For agents, use a separate logging sink.
PID 1 and stdout
In containers, PID 1’s stdout is the log stream. Don’t background and discard stdout. Don’t write secrets.
Minimal dual-sink setup
func setupLog(json bool) {
var h slog.Handler
opts := &slog.HandlerOptions{Level: slog.LevelInfo}
if json {
h = slog.NewJSONHandler(os.Stderr, opts)
} else {
h = slog.NewTextHandler(os.Stderr, opts)
}
slog.SetDefault(slog.New(h))
}CLI flag --log-json for production; text for local.
Rules of thumb
| Do | Don’t |
|---|---|
| stderr + supervisor for new apps | Invent custom log files without rotation plan |
| Structured key/value | Multiline free-form only |
| One correlation id field | Log passwords, tokens, cookies |
Document journalctl / kubectl logs |
Assume operators know your private log path |
Try next
- Run under
systemd-run --userand find logs withjournalctl --user.
- Emit JSON slog; parse with
jq.
- Add
versionandpidto every startup log line.