feat(logger): deliver stderr to systemd journal with per-record priority

When stderr is connected to journald, switch from the plain text handler
to the native journal protocol via github.com/systemd/slog-journal. Each
record now carries PRIORITY, MESSAGE, and slog attributes as structured
fields, so journalctl -p and LogLevelMax= in unit files filter by
severity instead of dropping everything at once.

Detection follows systemd.exec(5): parse JOURNAL_STREAM as <dev>:<ino>
and fstat(2) stderr to confirm the match, guarding against processes
that inherit the env var but have stderr redirected elsewhere. The
fstat path is Linux-only; other platforms fall through to the existing
text handler.

Also honour DEBUG_INVOCATION by raising stderr to Debug level, matching
the behaviour of systemd.service(5) RestartMode=debug.
This commit is contained in:
2026-04-12 14:01:07 -07:00
parent 79ccf33774
commit d8b9cddfb8
8 changed files with 235 additions and 1 deletions

41
logger/journald_linux.go Normal file
View File

@@ -0,0 +1,41 @@
//go:build linux
package logger
import (
"os"
"strconv"
"strings"
"syscall"
)
// stderrIsJournal reports whether os.Stderr is currently connected to
// the systemd journal. systemd.exec(5) sets JOURNAL_STREAM to
// "<dev>:<inode>" of the journal stream at service start. Detection
// must fstat(2) the actual stderr and compare — a child process may
// redirect stderr while inheriting the env var, so presence of the
// variable alone is not sufficient.
func stderrIsJournal() bool {
dev, ino, ok := parseJournalStream(os.Getenv("JOURNAL_STREAM"))
if !ok {
return false
}
var st syscall.Stat_t
if err := syscall.Fstat(int(os.Stderr.Fd()), &st); err != nil {
return false
}
return uint64(st.Dev) == dev && uint64(st.Ino) == ino
}
func parseJournalStream(v string) (dev, ino uint64, ok bool) {
sep := strings.IndexByte(v, ':')
if sep <= 0 || sep == len(v)-1 {
return 0, 0, false
}
d, err1 := strconv.ParseUint(v[:sep], 10, 64)
n, err2 := strconv.ParseUint(v[sep+1:], 10, 64)
if err1 != nil || err2 != nil {
return 0, 0, false
}
return d, n, true
}