Syslog, Journal, and Ops Logging

Updated

September 8, 2026

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

  1. Run under systemd-run --user and find logs with journalctl --user.
  2. Emit JSON slog; parse with jq.
  3. Add version and pid to every startup log line.