diff --git a/conf/configuration.go b/conf/configuration.go index 986ab7a5a..dacd42539 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -340,6 +340,10 @@ func Load(noConfigDump bool) { os.Exit(1) } log.SetOutput(out) + } else if os.Getenv("JOURNAL_STREAM") != "" { + // When running under systemd, prepend syslog priority prefixes so + // journald assigns the correct severity to each log line. + log.EnableJournalFormat() } log.SetLevelString(Server.LogLevel) diff --git a/log/journal.go b/log/journal.go new file mode 100644 index 000000000..f1c17d2e7 --- /dev/null +++ b/log/journal.go @@ -0,0 +1,41 @@ +package log + +import ( + "fmt" + + "github.com/sirupsen/logrus" +) + +// journalFormatter wraps a logrus.Formatter and prepends a syslog priority +// prefix () to each log line. When stderr is captured by systemd-journald, +// this prefix tells journald the correct severity for each message. +// +// See https://www.freedesktop.org/software/systemd/man/sd-daemon.html +type journalFormatter struct { + inner logrus.Formatter +} + +// levelToPriority maps logrus levels to syslog priority values. +// The mapping follows RFC 5424 severity levels. +var levelToPriority = map[logrus.Level]int{ + logrus.PanicLevel: 0, // emerg + logrus.FatalLevel: 2, // crit + logrus.ErrorLevel: 3, // err + logrus.WarnLevel: 4, // warning + logrus.InfoLevel: 6, // info + logrus.DebugLevel: 7, // debug + logrus.TraceLevel: 7, // debug +} + +func (f *journalFormatter) Format(entry *logrus.Entry) ([]byte, error) { + formatted, err := f.inner.Format(entry) + if err != nil { + return formatted, err + } + priority, ok := levelToPriority[entry.Level] + if !ok { + priority = 6 // default to info for unknown levels + } + prefix := []byte(fmt.Sprintf("<%d>", priority)) + return append(prefix, formatted...), nil +} diff --git a/log/journal_test.go b/log/journal_test.go new file mode 100644 index 000000000..770f12b6d --- /dev/null +++ b/log/journal_test.go @@ -0,0 +1,41 @@ +package log + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" +) + +var _ = Describe("journalFormatter", func() { + var formatter *journalFormatter + + BeforeEach(func() { + inner := &logrus.TextFormatter{ + DisableTimestamp: true, + DisableColors: true, + } + formatter = &journalFormatter{inner: inner} + }) + + DescribeTable("prefixes log lines with syslog priority", + func(level logrus.Level, expectedPrefix string) { + entry := &logrus.Entry{ + Logger: logrus.New(), + Level: level, + Message: "test message", + Data: logrus.Fields{}, + } + out, err := formatter.Format(entry) + Expect(err).ToNot(HaveOccurred()) + Expect(string(out)).To(HavePrefix(expectedPrefix)) + }, + Entry("error", logrus.ErrorLevel, "<3>"), + Entry("warning", logrus.WarnLevel, "<4>"), + Entry("info", logrus.InfoLevel, "<6>"), + Entry("debug", logrus.DebugLevel, "<7>"), + Entry("trace", logrus.TraceLevel, "<7>"), + Entry("fatal", logrus.FatalLevel, "<2>"), + Entry("panic", logrus.PanicLevel, "<0>"), + Entry("unknown level defaults to info", logrus.Level(99), "<6>"), + ) +}) diff --git a/log/log.go b/log/log.go index ef38484c8..2764d80e5 100644 --- a/log/log.go +++ b/log/log.go @@ -145,6 +145,15 @@ func SetOutput(w io.Writer) { defaultLogger.SetOutput(w) } +// EnableJournalFormat wraps the current logger formatter with syslog +// priority prefixes for systemd-journald. Only call this when output +// goes to stderr and JOURNAL_STREAM is set. +func EnableJournalFormat() { + loggerMu.Lock() + defer loggerMu.Unlock() + defaultLogger.Formatter = &journalFormatter{inner: defaultLogger.Formatter} +} + // Redact applies redaction to a single string func Redact(msg string) string { r, _ := redacted.redact(msg)