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