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.
42 lines
1.1 KiB
Go
42 lines
1.1 KiB
Go
//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
|
|
}
|