loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

log: journald integration (#2869)

Provide a bit more journald integration. Specifically:

- support emission of printk-style log level prefixes, documented in [`sd-daemon`(3)](https://man7.org/linux/man-pages/man3/sd-daemon.3.html#DESCRIPTION), that allow journald to automatically annotate stderr log lines with their level;
- add a new "journaldflags" item that is supposed to be used in place of "stdflags" when under journald to reduce log clutter (i. e. strip date/time info to avoid duplication, and use log level prefixes instead of textual log levels);
- detect whether stderr and/or stdout are attached to journald by parsing `$JOURNAL_STREAM` environment variable and adjust console logger defaults accordingly.

<!--start release-notes-assistant-->

## Draft release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
- [PR](https://codeberg.org/forgejo/forgejo/pulls/2869): <!--number 2869 --><!--line 0 --><!--description bG9nOiBqb3VybmFsZCBpbnRlZ3JhdGlvbg==-->log: journald integration<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2869
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Ivan Shapovalov <intelfx@intelfx.name>
Co-committed-by: Ivan Shapovalov <intelfx@intelfx.name>

authored by

Ivan Shapovalov
Ivan Shapovalov
and committed by
Earl Warren
012a1e04 a72763f5

+169 -18
+1 -1
custom/conf/app.example.ini
··· 635 635 ;[log.%(WriterMode)] 636 636 ;MODE=console/file/conn/... 637 637 ;LEVEL= 638 - ;FLAGS = stdflags 638 + ;FLAGS = stdflags or journald 639 639 ;EXPRESSION = 640 640 ;PREFIX = 641 641 ;COLORIZE = false
+8 -5
modules/log/color_console.go
··· 4 4 5 5 package log 6 6 7 - // CanColorStdout reports if we can color the Stdout 8 - // Although we could do terminal sniffing and the like - in reality 9 - // most tools on *nix are happy to display ansi colors. 10 - // We will terminal sniff on Windows in console_windows.go 7 + // CanColorStdout reports if we can use ANSI escape sequences on stdout 11 8 var CanColorStdout = true 12 9 13 - // CanColorStderr reports if we can color the Stderr 10 + // CanColorStderr reports if we can use ANSI escape sequences on stderr 14 11 var CanColorStderr = true 12 + 13 + // JournaldOnStdout reports whether stdout is attached to journald 14 + var JournaldOnStdout = false 15 + 16 + // JournaldOnStderr reports whether stderr is attached to journald 17 + var JournaldOnStderr = false
+50 -3
modules/log/color_console_other.go
··· 7 7 8 8 import ( 9 9 "os" 10 + "strconv" 11 + "strings" 12 + "syscall" 10 13 11 14 "github.com/mattn/go-isatty" 12 15 ) 13 16 17 + func journaldDevIno() (uint64, uint64, bool) { 18 + journaldStream := os.Getenv("JOURNAL_STREAM") 19 + if len(journaldStream) == 0 { 20 + return 0, 0, false 21 + } 22 + deviceStr, inodeStr, ok := strings.Cut(journaldStream, ":") 23 + device, err1 := strconv.ParseUint(deviceStr, 10, 64) 24 + inode, err2 := strconv.ParseUint(inodeStr, 10, 64) 25 + if !ok || err1 != nil || err2 != nil { 26 + return 0, 0, false 27 + } 28 + return device, inode, true 29 + } 30 + 31 + func fileStatDevIno(file *os.File) (uint64, uint64, bool) { 32 + info, err := file.Stat() 33 + if err != nil { 34 + return 0, 0, false 35 + } 36 + 37 + stat, ok := info.Sys().(*syscall.Stat_t) 38 + if !ok { 39 + return 0, 0, false 40 + } 41 + 42 + return stat.Dev, stat.Ino, true 43 + } 44 + 45 + func fileIsDevIno(file *os.File, dev, ino uint64) bool { 46 + fileDev, fileIno, ok := fileStatDevIno(file) 47 + return ok && dev == fileDev && ino == fileIno 48 + } 49 + 14 50 func init() { 15 - // when running gitea as a systemd unit with logging set to console, the output can not be colorized, 16 - // otherwise it spams the journal / syslog with escape sequences like "#033[0m#033[32mcmd/web.go:102:#033[32m" 17 - // this file covers non-windows platforms. 51 + // When forgejo is running under service supervisor (e.g. systemd) with logging 52 + // set to console, the output streams are typically captured into some logging 53 + // system (e.g. journald or syslog) instead of going to the terminal. Disable 54 + // usage of ANSI escape sequences if that's the case to avoid spamming 55 + // the journal or syslog with garbled mess e.g. `#033[0m#033[32mcmd/web.go:102:#033[32m`. 18 56 CanColorStdout = isatty.IsTerminal(os.Stdout.Fd()) 19 57 CanColorStderr = isatty.IsTerminal(os.Stderr.Fd()) 58 + 59 + // Furthermore, check if we are running under journald specifically so that 60 + // further output adjustments can be applied. Specifically, this changes 61 + // the console logger defaults to disable duplication of date/time info and 62 + // enable emission of special control sequences understood by journald 63 + // instead of ANSI colors. 64 + journalDev, journalIno, ok := journaldDevIno() 65 + JournaldOnStdout = ok && !CanColorStdout && fileIsDevIno(os.Stdout, journalDev, journalIno) 66 + JournaldOnStderr = ok && !CanColorStderr && fileIsDevIno(os.Stderr, journalDev, journalIno) 20 67 }
+9 -1
modules/log/event_format.go
··· 90 90 // EventFormatTextMessage makes the log message for a writer with its mode. This function is a copy of the original package 91 91 func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, msgArgs ...any) []byte { 92 92 buf := make([]byte, 0, 1024) 93 - buf = append(buf, mode.Prefix...) 94 93 t := event.Time 95 94 flags := mode.Flags.Bits() 95 + 96 + // if log level prefixes are enabled, the message must begin with the prefix, see sd_daemon(3) 97 + // "A line that is not prefixed will be logged at the default log level SD_INFO" 98 + if flags&Llevelprefix != 0 { 99 + prefix := event.Level.JournalPrefix() 100 + buf = append(buf, prefix...) 101 + } 102 + 103 + buf = append(buf, mode.Prefix...) 96 104 if flags&(Ldate|Ltime|Lmicroseconds) != 0 { 97 105 if mode.Colorize { 98 106 buf = append(buf, fgCyanBytes...)
+59 -2
modules/log/event_format_test.go
··· 35 35 "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), 36 36 ) 37 37 38 - assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1 38 + assert.Equal(t, `<3>[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1 39 39 stacktrace 40 40 41 41 `, string(res)) ··· 53 53 "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), 54 54 ) 55 55 56 - assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res)) 56 + assert.Equal(t, "<3>[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res)) 57 + } 58 + 59 + func TestEventFormatTextMessageStd(t *testing.T) { 60 + res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: LstdFlags}}, 61 + &Event{ 62 + Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC), 63 + Caller: "caller", 64 + Filename: "filename", 65 + Line: 123, 66 + GoroutinePid: "pid", 67 + Level: ERROR, 68 + Stacktrace: "stacktrace", 69 + }, 70 + "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), 71 + ) 72 + 73 + assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05 filename:123:caller [E] msg format: arg0 arg1 74 + stacktrace 75 + 76 + `, string(res)) 77 + 78 + res = EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: true, Flags: Flags{defined: true, flags: LstdFlags}}, 79 + &Event{ 80 + Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC), 81 + Caller: "caller", 82 + Filename: "filename", 83 + Line: 123, 84 + GoroutinePid: "pid", 85 + Level: ERROR, 86 + Stacktrace: "stacktrace", 87 + }, 88 + "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), 89 + ) 90 + 91 + assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res)) 92 + } 93 + 94 + func TestEventFormatTextMessageJournal(t *testing.T) { 95 + // TODO: it makes no sense to emit \n-containing messages to journal as they will get mangled 96 + // the proper way here is to attach the backtrace as structured metadata, but we can't do that via stderr 97 + res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: LjournaldFlags}}, 98 + &Event{ 99 + Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC), 100 + Caller: "caller", 101 + Filename: "filename", 102 + Line: 123, 103 + GoroutinePid: "pid", 104 + Level: ERROR, 105 + Stacktrace: "stacktrace", 106 + }, 107 + "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), 108 + ) 109 + 110 + assert.Equal(t, `<3>[PREFIX] msg format: arg0 arg1 111 + stacktrace 112 + 113 + `, string(res)) 57 114 }
+8 -4
modules/log/flags.go
··· 31 31 Llevelinitial // Initial character of the provided level in brackets, eg. [I] for info 32 32 Llevel // Provided level in brackets [INFO] 33 33 Lgopid // the Goroutine-PID of the context 34 + Llevelprefix // printk-style logging prefixes as documented in sd-daemon(3), used by journald 34 35 35 - Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename 36 - LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default 36 + Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename 37 + LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default 38 + LjournaldFlags = Llevelprefix 37 39 ) 38 40 39 41 const Ldefault = LstdFlags ··· 54 56 "utc": LUTC, 55 57 "levelinitial": Llevelinitial, 56 58 "level": Llevel, 59 + "levelprefix": Llevelprefix, 57 60 "gopid": Lgopid, 58 61 59 - "medfile": Lmedfile, 60 - "stdflags": LstdFlags, 62 + "medfile": Lmedfile, 63 + "stdflags": LstdFlags, 64 + "journaldflags": LjournaldFlags, 61 65 } 62 66 63 67 var flagComboToString = []struct {
+20
modules/log/level.go
··· 39 39 NONE: "none", 40 40 } 41 41 42 + // Machine-readable log level prefixes as defined in sd-daemon(3). 43 + // 44 + // "If a systemd service definition file is configured with StandardError=journal 45 + // or StandardError=kmsg (and similar with StandardOutput=), these prefixes can 46 + // be used to encode a log level in lines printed. <...> To use these prefixes 47 + // simply prefix every line with one of these strings. A line that is not prefixed 48 + // will be logged at the default log level SD_INFO." 49 + var toJournalPrefix = map[Level]string{ 50 + TRACE: "<7>", // SD_DEBUG 51 + DEBUG: "<6>", // SD_INFO 52 + INFO: "<5>", // SD_NOTICE 53 + WARN: "<4>", // SD_WARNING 54 + ERROR: "<3>", // SD_ERR 55 + FATAL: "<2>", // SD_CRIT 56 + } 57 + 42 58 var toLevel = map[string]Level{ 43 59 "undefined": UNDEFINED, 44 60 ··· 69 85 return s 70 86 } 71 87 return "info" 88 + } 89 + 90 + func (l Level) JournalPrefix() string { 91 + return toJournalPrefix[l] 72 92 } 73 93 74 94 func (l Level) ColorAttributes() []ColorAttribute {
+12 -2
modules/setting/log.go
··· 133 133 writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String())) 134 134 writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX") 135 135 writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION") 136 - writerMode.Flags = log.FlagsFromString(ConfigInheritedKeyString(sec, "FLAGS", defaultFlags)) 136 + // flags are updated and set below 137 137 138 138 switch writerType { 139 139 case "console": 140 - useStderr := ConfigInheritedKey(sec, "STDERR").MustBool(false) 140 + // if stderr is on journald, prefer stderr by default 141 + useStderr := ConfigInheritedKey(sec, "STDERR").MustBool(log.JournaldOnStderr) 141 142 defaultCanColor := log.CanColorStdout 143 + defaultJournald := log.JournaldOnStdout 142 144 if useStderr { 143 145 defaultCanColor = log.CanColorStderr 146 + defaultJournald = log.JournaldOnStderr 144 147 } 145 148 writerOption := log.WriterConsoleOption{Stderr: useStderr} 146 149 writerMode.Colorize = ConfigInheritedKey(sec, "COLORIZE").MustBool(defaultCanColor) 147 150 writerMode.WriterOption = writerOption 151 + // if we are ultimately on journald, update default flags 152 + if defaultJournald { 153 + defaultFlags = "journaldflags" 154 + } 148 155 case "file": 149 156 fileName := LogPrepareFilenameForWriter(ConfigInheritedKey(sec, "FILE_NAME").String(), defaultFilaName) 150 157 writerOption := log.WriterFileOption{} ··· 168 175 return "", "", writerMode, fmt.Errorf("invalid log writer type (mode): %s, maybe it needs something like 'MODE=file' in [log.%s] section", writerType, modeName) 169 176 } 170 177 } 178 + 179 + // set flags last because the console writer code may update default flags 180 + writerMode.Flags = log.FlagsFromString(ConfigInheritedKeyString(sec, "FLAGS", defaultFlags)) 171 181 172 182 return writerName, writerType, writerMode, nil 173 183 }
+2
modules/setting/setting.go
··· 230 230 231 231 // LoadSettingsForInstall initializes the settings for install 232 232 func LoadSettingsForInstall() { 233 + initAllLoggers() 234 + 233 235 loadDBSetting(CfgProvider) 234 236 loadServiceFrom(CfgProvider) 235 237 loadMailerFrom(CfgProvider)