···1+{ config, lib, pkgs, services, ... }:
2+with lib;
3+4+let
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+35+in {
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.extraUsers.${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 stdenv.lib.maintainers; [ florianjacob ];
245+ };
246+}
+43
pkgs/tools/system/journalwatch/default.nix
···0000000000000000000000000000000000000000000
···1+{ stdenv, buildPythonPackage, fetchurl, fetchgit, pythonOlder, systemd, pytest }:
2+3+buildPythonPackage rec {
4+ pname = "journalwatch";
5+ name = "${pname}-${version}";
6+ version = "1.1.0";
7+ disabled = pythonOlder "3.3";
8+9+10+ src = fetchurl {
11+ url = "https://github.com/The-Compiler/${pname}/archive/v${version}.tar.gz";
12+ sha512 = "3hvbgx95hjfivz9iv0hbhj720wvm32z86vj4a60lji2zdfpbqgr2b428lvg2cpvf71l2xn6ca5v0hzyz57qylgwqzgfrx7hqhl5g38s";
13+ };
14+15+ # can be removed post 1.1.0
16+ postPatch = ''
17+ substituteInPlace test_journalwatch.py \
18+ --replace "U Thu Jan 1 00:00:00 1970 prio foo [1337]" "U Thu Jan 1 00:00:00 1970 pprio foo [1337]"
19+ '';
20+21+22+ doCheck = true;
23+24+ checkPhase = ''
25+ pytest test_journalwatch.py
26+ '';
27+28+ buildInputs = [
29+ pytest
30+ ];
31+32+ propagatedBuildInputs = [
33+ systemd
34+ ];
35+36+37+ meta = with stdenv.lib; {
38+ description = "journalwatch is a tool to find error messages in the systemd journal.";
39+ homepage = "https://github.com/The-Compiler/journalwatch";
40+ license = licenses.gpl3Plus;
41+ maintainers = with maintainers; [ florianjacob ];
42+ };
43+}