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) }