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