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.
63 lines
1.6 KiB
Go
63 lines
1.6 KiB
Go
package logger
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
slogjournal "github.com/systemd/slog-journal"
|
|
)
|
|
|
|
// newJournalHandler returns a slog.Handler that delivers records to the
|
|
// systemd journal using the native protocol. It returns nil when stderr
|
|
// is not connected to journald, in which case the caller falls back to
|
|
// the stderr text handler.
|
|
func newJournalHandler(opts *slog.HandlerOptions) slog.Handler {
|
|
if !stderrIsJournal() {
|
|
return nil
|
|
}
|
|
h, err := slogjournal.NewHandler(&slogjournal.Options{
|
|
Level: opts.Level,
|
|
ReplaceAttr: journalReplaceAttr,
|
|
ReplaceGroup: sanitizeJournalKey,
|
|
})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return h
|
|
}
|
|
|
|
// journalReplaceAttr sanitizes slog attribute keys so they satisfy the
|
|
// journal's ^[A-Z_][A-Z0-9_]*$ constraint. Without this, otherwise
|
|
// valid attributes from third-party handlers (e.g. the trace_id/span_id
|
|
// injected by slogtraceid) are silently dropped by the journal.
|
|
func journalReplaceAttr(groups []string, a slog.Attr) slog.Attr {
|
|
a.Key = sanitizeJournalKey(a.Key)
|
|
return a
|
|
}
|
|
|
|
// sanitizeJournalKey maps an arbitrary string to a journal-legal key:
|
|
// upper-cases ASCII letters, replaces everything outside [A-Z0-9_] with
|
|
// '_', and prefixes '_' if the first byte is a digit.
|
|
func sanitizeJournalKey(k string) string {
|
|
if k == "" {
|
|
return k
|
|
}
|
|
b := make([]byte, 0, len(k)+1)
|
|
for i := 0; i < len(k); i++ {
|
|
c := k[i]
|
|
switch {
|
|
case c >= 'A' && c <= 'Z', c == '_':
|
|
b = append(b, c)
|
|
case c >= 'a' && c <= 'z':
|
|
b = append(b, c-'a'+'A')
|
|
case c >= '0' && c <= '9':
|
|
if i == 0 {
|
|
b = append(b, '_')
|
|
}
|
|
b = append(b, c)
|
|
default:
|
|
b = append(b, '_')
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|