at 21.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</option>. 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 description = '' 78 Mail address to send journalwatch reports from. 79 ''; 80 }; 81 82 mailTo = mkOption { 83 type = types.nullOr types.str; 84 default = null; 85 description = '' 86 Mail address to send journalwatch reports to. 87 ''; 88 }; 89 90 mailBinary = mkOption { 91 type = types.path; 92 default = "/run/wrappers/bin/sendmail"; 93 description = '' 94 Sendmail-compatible binary to be used to send the messages. 95 ''; 96 }; 97 98 extraConfig = mkOption { 99 type = types.str; 100 default = ""; 101 description = '' 102 Extra lines to be added verbatim to the journalwatch/config configuration file. 103 You can add any commandline argument to the config, without the '--'. 104 See <literal>journalwatch --help</literal> for all arguments and their description. 105 ''; 106 }; 107 108 filterBlocks = mkOption { 109 type = types.listOf (types.submodule { 110 options = { 111 match = mkOption { 112 type = types.str; 113 example = "SYSLOG_IDENTIFIER = systemd"; 114 description = '' 115 Syntax: <literal>field = value</literal> 116 Specifies the log entry <literal>field</literal> this block should apply to. 117 If the <literal>field</literal> of a message matches this <literal>value</literal>, 118 this patternBlock's <option>filters</option> are applied. 119 If <literal>value</literal> starts and ends with a slash, it is interpreted as 120 an extended python regular expression, if not, it's an exact match. 121 The journal fields are explained in systemd.journal-fields(7). 122 ''; 123 }; 124 125 filters = mkOption { 126 type = types.str; 127 example = '' 128 (Stopped|Stopping|Starting|Started) .* 129 (Reached target|Stopped target) .* 130 ''; 131 description = '' 132 The filters to apply on all messages which satisfy <option>match</option>. 133 Any of those messages that match any specified filter will be removed from journalwatch's output. 134 Each filter is an extended Python regular expression. 135 You can specify multiple filters and separate them by newlines. 136 Lines starting with '#' are comments. Inline-comments are not permitted. 137 ''; 138 }; 139 }; 140 }); 141 142 example = [ 143 # examples taken from upstream 144 { 145 match = "_SYSTEMD_UNIT = systemd-logind.service"; 146 filters = '' 147 New session [a-z]?\d+ of user \w+\. 148 Removed session [a-z]?\d+\. 149 ''; 150 } 151 152 { 153 match = "SYSLOG_IDENTIFIER = /(CROND|crond)/"; 154 filters = '' 155 pam_unix\(crond:session\): session (opened|closed) for user \w+ 156 \(\w+\) CMD .* 157 ''; 158 } 159 ]; 160 161 # another example from upstream. 162 # very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all. 163 default = [ 164 { 165 match = "SYSLOG_IDENTIFIER = systemd"; 166 filters = '' 167 (Stopped|Stopping|Starting|Started) .* 168 (Created slice|Removed slice) user-\d*\.slice\. 169 Received SIGRTMIN\+24 from PID .* 170 (Reached target|Stopped target) .* 171 Startup finished in \d*ms\. 172 ''; 173 } 174 ]; 175 176 177 description = '' 178 filterBlocks can be defined to blacklist journal messages which are not errors. 179 Each block matches on a log entry field, and the filters in that block then are matched 180 against all messages with a matching log entry field. 181 182 All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch. 183 If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default. 184 185 All regular expressions are extended Python regular expressions, for details 186 see: http://doc.pyschools.com/html/regex.html 187 ''; 188 }; 189 190 interval = mkOption { 191 type = types.str; 192 default = "hourly"; 193 description = '' 194 How often to run journalwatch. 195 196 The format is described in systemd.time(7). 197 ''; 198 }; 199 accuracy = mkOption { 200 type = types.str; 201 default = "10min"; 202 description = '' 203 The time window around the interval in which the journalwatch run will be scheduled. 204 205 The format is described in systemd.time(7). 206 ''; 207 }; 208 }; 209 }; 210 211 config = mkIf cfg.enable { 212 213 users.users.${user} = { 214 isSystemUser = true; 215 home = dataDir; 216 group = group; 217 }; 218 219 systemd.tmpfiles.rules = [ 220 # present since NixOS 19.09: remove old stateful symlink join directory, 221 # which has been replaced with the journalwatchConfigDir store path 222 "R ${dataDir}/config" 223 ]; 224 225 systemd.services.journalwatch = { 226 227 environment = { 228 # journalwatch stores the last processed timpestamp here 229 # the share subdirectory is historic now that config home lives in /nix/store, 230 # but moving this in a backwards-compatible way is much more work than what's justified 231 # for cleaning that up. 232 XDG_DATA_HOME = "${dataDir}/share"; 233 XDG_CONFIG_HOME = journalwatchConfigDir; 234 }; 235 serviceConfig = { 236 User = user; 237 Group = group; 238 Type = "oneshot"; 239 # requires a relative directory name to create beneath /var/lib 240 StateDirectory = user; 241 StateDirectoryMode = 0750; 242 ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail"; 243 # lowest CPU and IO priority, but both still in best-effort class to prevent starvation 244 Nice=19; 245 IOSchedulingPriority=7; 246 }; 247 }; 248 249 systemd.timers.journalwatch = { 250 description = "Periodic journalwatch run"; 251 wantedBy = [ "timers.target" ]; 252 timerConfig = { 253 OnCalendar = cfg.interval; 254 AccuracySec = cfg.accuracy; 255 Persistent = true; 256 }; 257 }; 258 259 }; 260 261 meta = { 262 maintainers = with lib.maintainers; [ florianjacob ]; 263 }; 264}