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.
77 lines
2.1 KiB
Go
77 lines
2.1 KiB
Go
//go:build linux
|
|
|
|
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseJournalStream(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantDev uint64
|
|
wantIno uint64
|
|
wantOK bool
|
|
}{
|
|
{"empty", "", 0, 0, false},
|
|
{"no separator", "12345", 0, 0, false},
|
|
{"leading colon", ":12345", 0, 0, false},
|
|
{"trailing colon", "8:", 0, 0, false},
|
|
{"non-numeric dev", "x:12345", 0, 0, false},
|
|
{"non-numeric ino", "8:x", 0, 0, false},
|
|
{"valid small", "8:12345", 8, 12345, true},
|
|
{"valid large", "18446744073709551615:1", 18446744073709551615, 1, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dev, ino, ok := parseJournalStream(tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Fatalf("ok=%v want %v (input=%q)", ok, tt.wantOK, tt.input)
|
|
}
|
|
if ok && (dev != tt.wantDev || ino != tt.wantIno) {
|
|
t.Fatalf("dev=%d ino=%d want dev=%d ino=%d", dev, ino, tt.wantDev, tt.wantIno)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStderrIsJournal_Unset(t *testing.T) {
|
|
t.Setenv("JOURNAL_STREAM", "")
|
|
if stderrIsJournal() {
|
|
t.Fatal("stderrIsJournal returned true with JOURNAL_STREAM unset")
|
|
}
|
|
}
|
|
|
|
func TestStderrIsJournal_Bogus(t *testing.T) {
|
|
t.Setenv("JOURNAL_STREAM", "not-a-valid-value")
|
|
if stderrIsJournal() {
|
|
t.Fatal("stderrIsJournal returned true with bogus JOURNAL_STREAM")
|
|
}
|
|
}
|
|
|
|
func TestStderrIsJournal_Mismatch(t *testing.T) {
|
|
// Pick impossibly high dev:inode that won't match real stderr.
|
|
t.Setenv("JOURNAL_STREAM", "999999999:999999999")
|
|
if stderrIsJournal() {
|
|
t.Fatal("stderrIsJournal returned true for mismatching dev:ino")
|
|
}
|
|
}
|
|
|
|
func TestStderrIsJournal_Match(t *testing.T) {
|
|
// Point JOURNAL_STREAM at the real stderr's dev:inode and confirm
|
|
// detection works. This exercises the fstat+compare path without
|
|
// needing an actual journal socket.
|
|
var st syscall.Stat_t
|
|
if err := syscall.Fstat(int(os.Stderr.Fd()), &st); err != nil {
|
|
t.Fatalf("fstat stderr: %v", err)
|
|
}
|
|
t.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", uint64(st.Dev), uint64(st.Ino)))
|
|
if !stderrIsJournal() {
|
|
t.Fatal("stderrIsJournal returned false when JOURNAL_STREAM matches stderr dev:ino")
|
|
}
|
|
}
|