at 25.11-pre 9.3 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.journalwatch; 9 user = "journalwatch"; 10 # for journal access 11 group = "systemd-journal"; 12 dataDir = "/var/lib/${user}"; 13 14 journalwatchConfig = pkgs.writeText "config" ( 15 '' 16 # (File Generated by NixOS journalwatch module.) 17 [DEFAULT] 18 mail_binary = ${cfg.mailBinary} 19 priority = ${toString cfg.priority} 20 mail_from = ${cfg.mailFrom} 21 '' 22 + lib.optionalString (cfg.mailTo != null) '' 23 mail_to = ${cfg.mailTo} 24 '' 25 + cfg.extraConfig 26 ); 27 28 journalwatchPatterns = pkgs.writeText "patterns" '' 29 # (File Generated by NixOS journalwatch module.) 30 31 ${mkPatterns cfg.filterBlocks} 32 ''; 33 34 # empty line at the end needed to to separate the blocks 35 mkPatterns = 36 filterBlocks: 37 lib.concatStringsSep "\n" ( 38 map (block: '' 39 ${block.match} 40 ${block.filters} 41 42 '') filterBlocks 43 ); 44 45 # can't use joinSymlinks directly, because when we point $XDG_CONFIG_HOME 46 # to the /nix/store path, we still need the subdirectory "journalwatch" inside that 47 # to match journalwatch's expectations 48 journalwatchConfigDir = 49 pkgs.runCommand "journalwatch-config" 50 { 51 preferLocalBuild = true; 52 allowSubstitutes = false; 53 } 54 '' 55 mkdir -p $out/journalwatch 56 ln -sf ${journalwatchConfig} $out/journalwatch/config 57 ln -sf ${journalwatchPatterns} $out/journalwatch/patterns 58 ''; 59 60in 61{ 62 options = { 63 services.journalwatch = { 64 enable = lib.mkOption { 65 type = lib.types.bool; 66 default = false; 67 description = '' 68 If enabled, periodically check the journal with journalwatch and report the results by mail. 69 ''; 70 }; 71 72 package = lib.mkPackageOption pkgs "journalwatch" { }; 73 74 priority = lib.mkOption { 75 type = lib.types.int; 76 default = 6; 77 description = '' 78 Lowest priority of message to be considered. 79 A value between 7 ("debug"), and 0 ("emerg"). Defaults to 6 ("info"). 80 If you don't care about anything with "info" priority, you can reduce 81 this to e.g. 5 ("notice") to considerably reduce the amount of 82 messages without needing many {option}`filterBlocks`. 83 ''; 84 }; 85 86 # HACK: this is a workaround for journalwatch's usage of socket.getfqdn() which always returns localhost if 87 # there's an alias for the localhost on a separate line in /etc/hosts, or take for ages if it's not present and 88 # then return something right-ish in the direction of /etc/hostname. Just bypass it completely. 89 mailFrom = lib.mkOption { 90 type = lib.types.str; 91 default = "journalwatch@${config.networking.hostName}"; 92 defaultText = lib.literalExpression ''"journalwatch@''${config.networking.hostName}"''; 93 description = '' 94 Mail address to send journalwatch reports from. 95 ''; 96 }; 97 98 mailTo = lib.mkOption { 99 type = lib.types.nullOr lib.types.str; 100 default = null; 101 description = '' 102 Mail address to send journalwatch reports to. 103 ''; 104 }; 105 106 mailBinary = lib.mkOption { 107 type = lib.types.path; 108 default = "/run/wrappers/bin/sendmail"; 109 description = '' 110 Sendmail-compatible binary to be used to send the messages. 111 ''; 112 }; 113 114 extraConfig = lib.mkOption { 115 type = lib.types.str; 116 default = ""; 117 description = '' 118 Extra lines to be added verbatim to the journalwatch/config configuration file. 119 You can add any commandline argument to the config, without the '--'. 120 See `journalwatch --help` for all arguments and their description. 121 ''; 122 }; 123 124 filterBlocks = lib.mkOption { 125 type = lib.types.listOf ( 126 lib.types.submodule { 127 options = { 128 match = lib.mkOption { 129 type = lib.types.str; 130 example = "SYSLOG_IDENTIFIER = systemd"; 131 description = '' 132 Syntax: `field = value` 133 Specifies the log entry `field` this block should apply to. 134 If the `field` of a message matches this `value`, 135 this patternBlock's {option}`filters` are applied. 136 If `value` starts and ends with a slash, it is interpreted as 137 an extended python regular expression, if not, it's an exact match. 138 The journal fields are explained in {manpage}`systemd.journal-fields(7)`. 139 ''; 140 }; 141 142 filters = lib.mkOption { 143 type = lib.types.str; 144 example = '' 145 (Stopped|Stopping|Starting|Started) .* 146 (Reached target|Stopped target) .* 147 ''; 148 description = '' 149 The filters to apply on all messages which satisfy {option}`match`. 150 Any of those messages that match any specified filter will be removed from journalwatch's output. 151 Each filter is an extended Python regular expression. 152 You can specify multiple filters and separate them by newlines. 153 Lines starting with '#' are comments. Inline-comments are not permitted. 154 ''; 155 }; 156 }; 157 } 158 ); 159 160 example = [ 161 # examples taken from upstream 162 { 163 match = "_SYSTEMD_UNIT = systemd-logind.service"; 164 filters = '' 165 New session [a-z]?\d+ of user \w+\. 166 Removed session [a-z]?\d+\. 167 ''; 168 } 169 170 { 171 match = "SYSLOG_IDENTIFIER = /(CROND|crond)/"; 172 filters = '' 173 pam_unix\(crond:session\): session (opened|closed) for user \w+ 174 \(\w+\) CMD .* 175 ''; 176 } 177 ]; 178 179 # another example from upstream. 180 # very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all. 181 default = [ 182 { 183 match = "SYSLOG_IDENTIFIER = systemd"; 184 filters = '' 185 (Stopped|Stopping|Starting|Started) .* 186 (Created slice|Removed slice) user-\d*\.slice\. 187 Received SIGRTMIN\+24 from PID .* 188 (Reached target|Stopped target) .* 189 Startup finished in \d*ms\. 190 ''; 191 } 192 ]; 193 194 description = '' 195 filterBlocks can be defined to blacklist journal messages which are not errors. 196 Each block matches on a log entry field, and the filters in that block then are matched 197 against all messages with a matching log entry field. 198 199 All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch. 200 If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default. 201 202 All regular expressions are extended Python regular expressions, for details 203 see: http://doc.pyschools.com/html/regex.html 204 ''; 205 }; 206 207 interval = lib.mkOption { 208 type = lib.types.str; 209 default = "hourly"; 210 description = '' 211 How often to run journalwatch. 212 213 The format is described in {manpage}`systemd.time(7)`. 214 ''; 215 }; 216 accuracy = lib.mkOption { 217 type = lib.types.str; 218 default = "10min"; 219 description = '' 220 The time window around the interval in which the journalwatch run will be scheduled. 221 222 The format is described in {manpage}`systemd.time(7)`. 223 ''; 224 }; 225 }; 226 }; 227 228 config = lib.mkIf cfg.enable { 229 230 users.users.${user} = { 231 isSystemUser = true; 232 home = dataDir; 233 group = group; 234 }; 235 236 systemd.tmpfiles.rules = [ 237 # present since NixOS 19.09: remove old stateful symlink join directory, 238 # which has been replaced with the journalwatchConfigDir store path 239 "R ${dataDir}/config" 240 ]; 241 242 systemd.services.journalwatch = { 243 244 environment = { 245 # journalwatch stores the last processed timpestamp here 246 # the share subdirectory is historic now that config home lives in /nix/store, 247 # but moving this in a backwards-compatible way is much more work than what's justified 248 # for cleaning that up. 249 XDG_DATA_HOME = "${dataDir}/share"; 250 XDG_CONFIG_HOME = journalwatchConfigDir; 251 }; 252 serviceConfig = { 253 User = user; 254 Group = group; 255 Type = "oneshot"; 256 # requires a relative directory name to create beneath /var/lib 257 StateDirectory = user; 258 StateDirectoryMode = "0750"; 259 ExecStart = "${lib.getExe cfg.package} mail"; 260 # lowest CPU and IO priority, but both still in best-effort class to prevent starvation 261 Nice = 19; 262 IOSchedulingPriority = 7; 263 }; 264 }; 265 266 systemd.timers.journalwatch = { 267 description = "Periodic journalwatch run"; 268 wantedBy = [ "timers.target" ]; 269 timerConfig = { 270 OnCalendar = cfg.interval; 271 AccuracySec = cfg.accuracy; 272 Persistent = true; 273 }; 274 }; 275 276 }; 277 278 meta = { 279 maintainers = with lib.maintainers; [ florianjacob ]; 280 }; 281}