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

View File

@@ -128,6 +128,15 @@ func setupStdErrHandler() slog.Handler {
}
}
// DEBUG_INVOCATION is set by systemd on a restart attempt of a unit
// configured with RestartMode=debug when the previous start failed.
// Treat it as a request to raise stderr verbosity to Debug so the
// next failing cycle yields maximum diagnostics. OTLPLevel is left
// alone — that stays under server/admin control.
if os.Getenv("DEBUG_INVOCATION") != "" {
Level.Set(slog.LevelDebug)
}
logOptions := &slog.HandlerOptions{Level: Level}
if len(os.Getenv("INVOCATION_ID")) > 0 {
@@ -137,8 +146,15 @@ func setupStdErrHandler() slog.Handler {
logOptions.ReplaceAttr = logRemoveTime
}
var base slog.Handler
if h := newJournalHandler(logOptions); h != nil {
base = h
} else {
base = slog.NewTextHandler(os.Stderr, logOptions)
}
logHandler := slogtraceid.OtelHandler{
Next: slog.NewTextHandler(os.Stderr, logOptions),
Next: base,
}
return logHandler