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

28
logger/journald_test.go Normal file
View File

@@ -0,0 +1,28 @@
package logger
import "testing"
func TestSanitizeJournalKey(t *testing.T) {
tests := []struct {
in, want string
}{
{"", ""},
{"MESSAGE", "MESSAGE"},
{"trace_id", "TRACE_ID"},
{"span.id", "SPAN_ID"},
{"http-method", "HTTP_METHOD"},
{"a", "A"},
{"123abc", "_123ABC"},
{"weird!@#key", "WEIRD___KEY"},
{"_leading", "_LEADING"},
{"MiXeD_Case", "MIXED_CASE"},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
got := sanitizeJournalKey(tt.in)
if got != tt.want {
t.Errorf("sanitizeJournalKey(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}